@lifeaitools/clauth 0.3.12 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/commands/serve.js +1653 -1213
- package/cli/index.js +496 -493
- package/package.json +1 -1
package/cli/index.js
CHANGED
|
@@ -1,493 +1,496 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// cli/index.js — clauth entry point
|
|
3
|
-
|
|
4
|
-
import { Command } from "commander";
|
|
5
|
-
import chalk from "chalk";
|
|
6
|
-
import ora from "ora";
|
|
7
|
-
import inquirer from "inquirer";
|
|
8
|
-
import Conf from "conf";
|
|
9
|
-
import { getConfOptions } from "./conf-path.js";
|
|
10
|
-
import { getMachineHash, deriveToken, deriveSeedHash } from "./fingerprint.js";
|
|
11
|
-
import * as api from "./api.js";
|
|
12
|
-
import os from "os";
|
|
13
|
-
|
|
14
|
-
const config = new Conf(getConfOptions());
|
|
15
|
-
const VERSION = "0.
|
|
16
|
-
|
|
17
|
-
// ============================================================
|
|
18
|
-
// Password prompt helper
|
|
19
|
-
// ============================================================
|
|
20
|
-
async function promptPassword(message = "clauth password") {
|
|
21
|
-
const { pw } = await inquirer.prompt([{
|
|
22
|
-
type: "password",
|
|
23
|
-
name: "pw",
|
|
24
|
-
message,
|
|
25
|
-
mask: "*",
|
|
26
|
-
validate: v => v.length >= 8 || "Password must be at least 8 characters"
|
|
27
|
-
}]);
|
|
28
|
-
return pw;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// ============================================================
|
|
32
|
-
// Auth helper — get pw + derive token
|
|
33
|
-
// ============================================================
|
|
34
|
-
async function getAuth(pw) {
|
|
35
|
-
const password = pw || await promptPassword();
|
|
36
|
-
const machineHash = getMachineHash();
|
|
37
|
-
const { token, timestamp } = deriveToken(password, machineHash);
|
|
38
|
-
return { password, machineHash, token, timestamp };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ============================================================
|
|
42
|
-
// Program
|
|
43
|
-
// ============================================================
|
|
44
|
-
const program = new Command();
|
|
45
|
-
|
|
46
|
-
program
|
|
47
|
-
.name("clauth")
|
|
48
|
-
.version(VERSION)
|
|
49
|
-
.description(chalk.cyan("🔐 clauth") + " — Hardware-bound credential vault for LIFEAI infrastructure");
|
|
50
|
-
|
|
51
|
-
// ──────────────────────────────────────────────
|
|
52
|
-
// clauth install (Supabase provisioning + skill install + test)
|
|
53
|
-
// ──────────────────────────────────────────────
|
|
54
|
-
import { runInstall } from './commands/install.js';
|
|
55
|
-
import { runUninstall } from './commands/uninstall.js';
|
|
56
|
-
import { runScrub } from './commands/scrub.js';
|
|
57
|
-
import { runServe } from './commands/serve.js';
|
|
58
|
-
|
|
59
|
-
program
|
|
60
|
-
.command('install')
|
|
61
|
-
.description('Provision Supabase, deploy Edge Function, install Claude skill')
|
|
62
|
-
.option('--ref <ref>', 'Supabase project ref')
|
|
63
|
-
.option('--pat <pat>', 'Supabase Personal Access Token')
|
|
64
|
-
.action(async (opts) => {
|
|
65
|
-
await runInstall(opts);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
program
|
|
69
|
-
.command('uninstall')
|
|
70
|
-
.description('Full teardown — drop DB objects, Edge Function, secrets, skill, config')
|
|
71
|
-
.option('--ref <ref>', 'Supabase project ref')
|
|
72
|
-
.option('--pat <pat>', 'Supabase Personal Access Token (required)')
|
|
73
|
-
.option('--yes', 'Skip confirmation prompt')
|
|
74
|
-
.action(async (opts) => {
|
|
75
|
-
if (!opts.yes) {
|
|
76
|
-
const inquirerMod = await import('inquirer');
|
|
77
|
-
const { confirm } = await inquirerMod.default.prompt([{
|
|
78
|
-
type: 'input',
|
|
79
|
-
name: 'confirm',
|
|
80
|
-
message: chalk.red('Type "CONFIRM UNINSTALL" to proceed:'),
|
|
81
|
-
}]);
|
|
82
|
-
if (confirm !== 'CONFIRM UNINSTALL') {
|
|
83
|
-
console.log(chalk.yellow('\n Uninstall cancelled.\n'));
|
|
84
|
-
process.exit(0);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
await runUninstall(opts);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// ──────────────────────────────────────────────
|
|
91
|
-
// clauth setup
|
|
92
|
-
// ──────────────────────────────────────────────
|
|
93
|
-
program
|
|
94
|
-
.command("setup")
|
|
95
|
-
.description("Register this machine with the vault (run after clauth install)")
|
|
96
|
-
.option("--admin-token <token>", "Bootstrap token (from clauth install output)")
|
|
97
|
-
.option("--label <label>", "Human label for this machine")
|
|
98
|
-
.option("-p, --pw <password>", "Password (skip interactive prompt)")
|
|
99
|
-
.action(async (opts) => {
|
|
100
|
-
console.log(chalk.cyan("\n🔐 clauth setup\n"));
|
|
101
|
-
|
|
102
|
-
// URL + anon key already saved by clauth install — fail fast if missing
|
|
103
|
-
const savedUrl = config.get("supabase_url");
|
|
104
|
-
const savedAnon = config.get("supabase_anon_key");
|
|
105
|
-
if (!savedUrl || !savedAnon) {
|
|
106
|
-
console.log(chalk.yellow(" Supabase config not found. Run clauth install first.\n"));
|
|
107
|
-
process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
console.log(chalk.gray(` Project: ${savedUrl}\n`));
|
|
110
|
-
|
|
111
|
-
let answers;
|
|
112
|
-
if (opts.pw && opts.adminToken) {
|
|
113
|
-
// Non-interactive mode — all flags provided
|
|
114
|
-
answers = { label: opts.label || os.hostname(), pw: opts.pw, adminTk: opts.adminToken };
|
|
115
|
-
} else {
|
|
116
|
-
answers = await inquirer.prompt([
|
|
117
|
-
{ type: "input", name: "label", message: "Machine label:", default: opts.label || os.hostname() },
|
|
118
|
-
{ type: "password", name: "pw", message: "Set password:", mask: "*", default: opts.pw || "" },
|
|
119
|
-
{ type: "password", name: "adminTk", message: "Bootstrap token:", mask: "*",
|
|
120
|
-
default: opts.adminToken || "" },
|
|
121
|
-
]);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const spinner = ora("Registering machine with vault...").start();
|
|
125
|
-
try {
|
|
126
|
-
const machineHash = getMachineHash();
|
|
127
|
-
const seedHash = deriveSeedHash(machineHash, answers.pw);
|
|
128
|
-
const result = await api.registerMachine(machineHash, seedHash, answers.label, answers.adminTk);
|
|
129
|
-
if (result.error) throw new Error(result.error);
|
|
130
|
-
spinner.succeed(chalk.green(`Machine registered: ${machineHash.slice(0,12)}...`));
|
|
131
|
-
console.log(chalk.green("\n✓ clauth is ready.\n"));
|
|
132
|
-
console.log(chalk.cyan(" clauth test — verify connection"));
|
|
133
|
-
console.log(chalk.cyan(" clauth status — see all services\n"));
|
|
134
|
-
} catch (err) {
|
|
135
|
-
spinner.fail(chalk.red(`Setup failed: ${err.message}`));
|
|
136
|
-
process.exit(1);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// ──────────────────────────────────────────────
|
|
141
|
-
// clauth status
|
|
142
|
-
// ──────────────────────────────────────────────
|
|
143
|
-
program
|
|
144
|
-
.command("status")
|
|
145
|
-
.description("Show all services and their state")
|
|
146
|
-
.option("-p, --pw <password>", "Password (or will prompt)")
|
|
147
|
-
.action(async (opts) => {
|
|
148
|
-
const auth = await getAuth(opts.pw);
|
|
149
|
-
const spinner = ora("Fetching service status...").start();
|
|
150
|
-
try {
|
|
151
|
-
const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp);
|
|
152
|
-
spinner.stop();
|
|
153
|
-
if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); return; }
|
|
154
|
-
|
|
155
|
-
console.log(chalk.cyan("\n🔐 clauth service status\n"));
|
|
156
|
-
console.log(
|
|
157
|
-
chalk.bold(
|
|
158
|
-
" " + "SERVICE".padEnd(20) + "TYPE".padEnd(12) + "STATUS".padEnd(12) +
|
|
159
|
-
"KEY STORED".padEnd(12) + "LAST RETRIEVED"
|
|
160
|
-
)
|
|
161
|
-
);
|
|
162
|
-
console.log(" " + "─".repeat(72));
|
|
163
|
-
|
|
164
|
-
for (const s of result.services || []) {
|
|
165
|
-
const status = s.enabled
|
|
166
|
-
? chalk.green("ACTIVE".padEnd(12))
|
|
167
|
-
: s.vault_key
|
|
168
|
-
? chalk.yellow("SUSPENDED".padEnd(12))
|
|
169
|
-
: chalk.gray("NO KEY".padEnd(12));
|
|
170
|
-
const hasKey = s.vault_key ? chalk.green("✓".padEnd(12)) : chalk.gray("—".padEnd(12));
|
|
171
|
-
const lastGet = s.last_retrieved
|
|
172
|
-
? new Date(s.last_retrieved).toLocaleDateString()
|
|
173
|
-
: chalk.gray("never");
|
|
174
|
-
|
|
175
|
-
console.log(` ${s.name.padEnd(20)}${s.key_type.padEnd(12)}${status}${hasKey}${lastGet}`);
|
|
176
|
-
}
|
|
177
|
-
console.log();
|
|
178
|
-
} catch (err) {
|
|
179
|
-
spinner.fail(chalk.red(err.message));
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// ──────────────────────────────────────────────
|
|
184
|
-
// clauth write pw <new_password>
|
|
185
|
-
// clauth write params
|
|
186
|
-
// clauth write key <service> <value>
|
|
187
|
-
// ──────────────────────────────────────────────
|
|
188
|
-
const writeCmd = program.command("write").description("Write credentials or update auth parameters");
|
|
189
|
-
|
|
190
|
-
writeCmd
|
|
191
|
-
.command("pw [newpw]")
|
|
192
|
-
.description("Set or update clauth master password")
|
|
193
|
-
.action(async (newpw) => {
|
|
194
|
-
console.log(chalk.cyan("\n🔐 clauth write pw\n"));
|
|
195
|
-
const current = await promptPassword("Current password (to verify)");
|
|
196
|
-
const pw = newpw || (await inquirer.prompt([
|
|
197
|
-
{ type: "password", name: "p", message: "New password:", mask: "*" },
|
|
198
|
-
{ type: "password", name: "c", message: "Confirm new password:", mask: "*" }
|
|
199
|
-
]).then(a => { if (a.p !== a.c) { console.log(chalk.red("Passwords don't match")); process.exit(1); } return a.p; }));
|
|
200
|
-
|
|
201
|
-
// Re-register machine with new seed hash
|
|
202
|
-
const machineHash = getMachineHash();
|
|
203
|
-
const newSeedHash = deriveSeedHash(machineHash, pw);
|
|
204
|
-
const { token, timestamp } = deriveToken(current, machineHash);
|
|
205
|
-
const adminToken = await inquirer.prompt([{
|
|
206
|
-
type: "password", name: "t", message: "Admin bootstrap token (required for re-registration):", mask: "*"
|
|
207
|
-
}]).then(a => a.t);
|
|
208
|
-
|
|
209
|
-
const spinner = ora("Updating password and re-registering machine...").start();
|
|
210
|
-
try {
|
|
211
|
-
const result = await api.registerMachine(machineHash, newSeedHash, null, adminToken);
|
|
212
|
-
if (result.error) throw new Error(result.error);
|
|
213
|
-
spinner.succeed(chalk.green("Password updated and machine re-registered."));
|
|
214
|
-
} catch (err) {
|
|
215
|
-
spinner.fail(chalk.red(err.message));
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
writeCmd
|
|
220
|
-
.command("params")
|
|
221
|
-
.description("Re-read hardware fingerprint (use after hardware change)")
|
|
222
|
-
.action(async () => {
|
|
223
|
-
const spinner = ora("Reading hardware fingerprint...").start();
|
|
224
|
-
try {
|
|
225
|
-
const hash = getMachineHash();
|
|
226
|
-
spinner.succeed(chalk.green(`Machine hash: ${hash.slice(0,16)}...`));
|
|
227
|
-
console.log(chalk.gray("Full hash: " + hash));
|
|
228
|
-
} catch (err) {
|
|
229
|
-
spinner.fail(chalk.red(err.message));
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
writeCmd
|
|
234
|
-
.command("key <service> [value]")
|
|
235
|
-
.description("Write a credential into vault for a service")
|
|
236
|
-
.option("-p, --pw <password>", "Password")
|
|
237
|
-
.action(async (service, value, opts) => {
|
|
238
|
-
const auth = await getAuth(opts.pw);
|
|
239
|
-
let val = value;
|
|
240
|
-
if (!val) {
|
|
241
|
-
const { v } = await inquirer.prompt([{ type: "password", name: "v", message: `Value for ${service}:`, mask: "*" }]);
|
|
242
|
-
val = v;
|
|
243
|
-
}
|
|
244
|
-
const spinner = ora(`Writing key for ${service}...`).start();
|
|
245
|
-
try {
|
|
246
|
-
const result = await api.write(auth.password, auth.machineHash, auth.token, auth.timestamp, service, val);
|
|
247
|
-
if (result.error) throw new Error(result.error);
|
|
248
|
-
spinner.succeed(chalk.green(`Key stored in vault: auth.${service}`));
|
|
249
|
-
} catch (err) {
|
|
250
|
-
spinner.fail(chalk.red(err.message));
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
// ──────────────────────────────────────────────
|
|
255
|
-
// clauth enable <service|all>
|
|
256
|
-
// clauth disable <service|all>
|
|
257
|
-
// ──────────────────────────────────────────────
|
|
258
|
-
program
|
|
259
|
-
.command("enable <service>")
|
|
260
|
-
.description("Enable a service (or 'all')")
|
|
261
|
-
.option("-p, --pw <password>")
|
|
262
|
-
.action(async (service, opts) => {
|
|
263
|
-
const auth = await getAuth(opts.pw);
|
|
264
|
-
const spinner = ora(`Enabling ${service}...`).start();
|
|
265
|
-
try {
|
|
266
|
-
const result = await api.enable(auth.password, auth.machineHash, auth.token, auth.timestamp, service, true);
|
|
267
|
-
if (result.error) throw new Error(result.error);
|
|
268
|
-
spinner.succeed(chalk.green(`Enabled: ${service}`));
|
|
269
|
-
} catch (err) { spinner.fail(chalk.red(err.message)); }
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
program
|
|
273
|
-
.command("disable <service>")
|
|
274
|
-
.description("Disable a service (or 'all')")
|
|
275
|
-
.option("-p, --pw <password>")
|
|
276
|
-
.action(async (service, opts) => {
|
|
277
|
-
const auth = await getAuth(opts.pw);
|
|
278
|
-
const spinner = ora(`Disabling ${service}...`).start();
|
|
279
|
-
try {
|
|
280
|
-
const result = await api.enable(auth.password, auth.machineHash, auth.token, auth.timestamp, service, false);
|
|
281
|
-
if (result.error) throw new Error(result.error);
|
|
282
|
-
spinner.succeed(chalk.yellow(`Disabled: ${service}`));
|
|
283
|
-
} catch (err) { spinner.fail(chalk.red(err.message)); }
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
// ──────────────────────────────────────────────
|
|
287
|
-
// clauth add service <name>
|
|
288
|
-
// clauth remove service <name>
|
|
289
|
-
// clauth list services
|
|
290
|
-
// ──────────────────────────────────────────────
|
|
291
|
-
const addCmd = program.command("add").description("Add resources to the registry");
|
|
292
|
-
|
|
293
|
-
addCmd
|
|
294
|
-
.command("service <name>")
|
|
295
|
-
.description("Register a new service slot")
|
|
296
|
-
.option("--type <type>", "Key type: token | keypair | connstring | oauth")
|
|
297
|
-
.option("--label <label>", "Human-readable label")
|
|
298
|
-
.option("--description <desc>", "Description")
|
|
299
|
-
.option("-p, --pw <password>")
|
|
300
|
-
.action(async (name, opts) => {
|
|
301
|
-
const auth = await getAuth(opts.pw);
|
|
302
|
-
let answers;
|
|
303
|
-
if (opts.type && opts.label) {
|
|
304
|
-
// Non-interactive — all flags provided
|
|
305
|
-
answers = { label: opts.label, key_type: opts.type, desc: opts.description || "" };
|
|
306
|
-
} else {
|
|
307
|
-
answers = await inquirer.prompt([
|
|
308
|
-
{ type: "input", name: "label", message: "Label:", default: opts.label || name },
|
|
309
|
-
{ type: "list", name: "key_type", message: "Key type:", choices: ["token","keypair","connstring","oauth"], default: opts.type || "token" },
|
|
310
|
-
{ type: "input", name: "desc", message: "Description (optional):", default: opts.description || "" }
|
|
311
|
-
]);
|
|
312
|
-
}
|
|
313
|
-
const spinner = ora(`Adding service: ${name}...`).start();
|
|
314
|
-
try {
|
|
315
|
-
const result = await api.addService(
|
|
316
|
-
auth.password, auth.machineHash, auth.token, auth.timestamp,
|
|
317
|
-
name, answers.label, answers.key_type, answers.desc
|
|
318
|
-
);
|
|
319
|
-
if (result.error) throw new Error(result.error);
|
|
320
|
-
spinner.succeed(chalk.green(`Service added: ${name} (${answers.key_type})`));
|
|
321
|
-
console.log(chalk.gray(` Next: clauth write key ${name}`));
|
|
322
|
-
} catch (err) { spinner.fail(chalk.red(err.message)); }
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
const removeCmd = program.command("remove").description("Remove resources from the registry");
|
|
326
|
-
|
|
327
|
-
removeCmd
|
|
328
|
-
.command("service <name>")
|
|
329
|
-
.description("Remove a service and its key from vault")
|
|
330
|
-
.option("-p, --pw <password>")
|
|
331
|
-
.action(async (name, opts) => {
|
|
332
|
-
const { confirm } = await inquirer.prompt([{
|
|
333
|
-
type: "input", name: "confirm",
|
|
334
|
-
message: chalk.red(`Type "CONFIRM REMOVE ${name.toUpperCase()}" to proceed:`)
|
|
335
|
-
}]);
|
|
336
|
-
const auth = await getAuth(opts.pw);
|
|
337
|
-
const spinner = ora(`Removing ${name}...`).start();
|
|
338
|
-
try {
|
|
339
|
-
const result = await api.removeService(auth.password, auth.machineHash, auth.token, auth.timestamp, name, confirm);
|
|
340
|
-
if (result.error) throw new Error(result.error);
|
|
341
|
-
spinner.succeed(chalk.yellow(`Removed: ${name}`));
|
|
342
|
-
} catch (err) { spinner.fail(chalk.red(err.message)); }
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
program
|
|
346
|
-
.command("list")
|
|
347
|
-
.description("List all registered services")
|
|
348
|
-
.option("-p, --pw <password>")
|
|
349
|
-
.action(async (opts) => {
|
|
350
|
-
const auth = await getAuth(opts.pw);
|
|
351
|
-
const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp);
|
|
352
|
-
if (result.error) { console.log(chalk.red(result.error)); return; }
|
|
353
|
-
console.log(chalk.cyan("\n Registered services:\n"));
|
|
354
|
-
for (const s of result.services || []) {
|
|
355
|
-
console.log(` ${chalk.bold(s.name.padEnd(20))} ${chalk.gray(s.key_type.padEnd(12))} ${chalk.gray(s.description || "")}`);
|
|
356
|
-
}
|
|
357
|
-
console.log();
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
// ──────────────────────────────────────────────
|
|
361
|
-
// clauth test <service|all>
|
|
362
|
-
// ──────────────────────────────────────────────
|
|
363
|
-
program
|
|
364
|
-
.command("test [service]")
|
|
365
|
-
.description("Test HMAC handshake — no key returned")
|
|
366
|
-
.option("-p, --pw <password>")
|
|
367
|
-
.action(async (service, opts) => {
|
|
368
|
-
const auth = await getAuth(opts.pw);
|
|
369
|
-
const spinner = ora("Testing auth handshake...").start();
|
|
370
|
-
try {
|
|
371
|
-
const result = await api.test(auth.password, auth.machineHash, auth.token, auth.timestamp);
|
|
372
|
-
if (result.error) throw new Error(`${result.error}: ${result.reason}`);
|
|
373
|
-
spinner.succeed(chalk.green("PASS — HMAC validated"));
|
|
374
|
-
console.log(chalk.gray(` Machine: ${auth.machineHash.slice(0,16)}...`));
|
|
375
|
-
console.log(chalk.gray(` Window: ${new Date(result.timestamp).toISOString()}`));
|
|
376
|
-
} catch (err) {
|
|
377
|
-
spinner.fail(chalk.red("FAIL — " + err.message));
|
|
378
|
-
}
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
// ──────────────────────────────────────────────
|
|
382
|
-
// clauth get <service>
|
|
383
|
-
// ──────────────────────────────────────────────
|
|
384
|
-
program
|
|
385
|
-
.command("get <service>")
|
|
386
|
-
.description("Retrieve a key from vault")
|
|
387
|
-
.option("-p, --pw <password>")
|
|
388
|
-
.option("--json", "Output raw JSON")
|
|
389
|
-
.action(async (service, opts) => {
|
|
390
|
-
const auth = await getAuth(opts.pw);
|
|
391
|
-
const spinner = ora(`Retrieving ${service}...`).start();
|
|
392
|
-
try {
|
|
393
|
-
const result = await api.retrieve(auth.password, auth.machineHash, auth.token, auth.timestamp, service);
|
|
394
|
-
spinner.stop();
|
|
395
|
-
if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); return; }
|
|
396
|
-
if (opts.json) {
|
|
397
|
-
console.log(JSON.stringify(result, null, 2));
|
|
398
|
-
} else {
|
|
399
|
-
console.log(chalk.cyan(`\n🔑 ${service} (${result.key_type})\n`));
|
|
400
|
-
const val = typeof result.value === "string" ? result.value : JSON.stringify(result.value, null, 2);
|
|
401
|
-
console.log(val);
|
|
402
|
-
console.log();
|
|
403
|
-
}
|
|
404
|
-
} catch (err) {
|
|
405
|
-
spinner.fail(chalk.red(err.message));
|
|
406
|
-
}
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
// ──────────────────────────────────────────────
|
|
410
|
-
// clauth revoke <service|all>
|
|
411
|
-
// ──────────────────────────────────────────────
|
|
412
|
-
program
|
|
413
|
-
.command("revoke <service>")
|
|
414
|
-
.description("Delete key from vault (destructive)")
|
|
415
|
-
.option("-p, --pw <password>")
|
|
416
|
-
.action(async (service, opts) => {
|
|
417
|
-
const phrase = service === "all" ? "CONFIRM REVOKE ALL" : `CONFIRM REVOKE ${service.toUpperCase()}`;
|
|
418
|
-
const { confirm } = await inquirer.prompt([{
|
|
419
|
-
type: "input", name: "confirm",
|
|
420
|
-
message: chalk.red(`Type "${phrase}" to proceed:`)
|
|
421
|
-
}]);
|
|
422
|
-
const auth = await getAuth(opts.pw);
|
|
423
|
-
const spinner = ora(`Revoking ${service}...`).start();
|
|
424
|
-
try {
|
|
425
|
-
const result = await api.revoke(auth.password, auth.machineHash, auth.token, auth.timestamp, service, confirm);
|
|
426
|
-
if (result.error) throw new Error(result.error);
|
|
427
|
-
spinner.succeed(chalk.yellow(`Revoked: ${service}`));
|
|
428
|
-
} catch (err) { spinner.fail(chalk.red(err.message)); }
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
// ──────────────────────────────────────────────
|
|
432
|
-
// clauth scrub [target]
|
|
433
|
-
// ──────────────────────────────────────────────
|
|
434
|
-
program
|
|
435
|
-
.command("scrub [target]")
|
|
436
|
-
.description("Scrub credentials from Claude Code transcript logs (no auth required)")
|
|
437
|
-
.option("--force", "Rescrub files even if already marked clean")
|
|
438
|
-
.addHelpText("after", `
|
|
439
|
-
Examples:
|
|
440
|
-
clauth scrub Scrub the most recent (active) transcript
|
|
441
|
-
clauth scrub <file> Scrub a specific .jsonl file
|
|
442
|
-
clauth scrub all Scrub every transcript in ~/.claude/projects/
|
|
443
|
-
clauth scrub all --force Rescrub all files (ignore markers)
|
|
444
|
-
`)
|
|
445
|
-
.action(async (target, opts) => {
|
|
446
|
-
await runScrub(target, opts);
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
// ──────────────────────────────────────────────
|
|
450
|
-
// clauth --help override banner
|
|
451
|
-
// ──────────────────────────────────────────────
|
|
452
|
-
program.addHelpText("beforeAll", chalk.cyan(`
|
|
453
|
-
██████╗██╗ █████╗ ██╗ ██╗████████╗██╗ ██╗
|
|
454
|
-
██╔════╝██║ ██╔══██╗██║ ██║╚══██╔══╝██║ ██║
|
|
455
|
-
██║ ██║ ███████║██║ ██║ ██║ ███████║
|
|
456
|
-
██║ ██║ ██╔══██║██║ ██║ ██║ ██╔══██║
|
|
457
|
-
╚██████╗███████╗██║ ██║╚██████╔╝ ██║ ██║ ██║
|
|
458
|
-
╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝
|
|
459
|
-
v${VERSION} — LIFEAI Credential Vault
|
|
460
|
-
`));
|
|
461
|
-
|
|
462
|
-
// ──────────────────────────────────────────────
|
|
463
|
-
// clauth serve [action]
|
|
464
|
-
// ──────────────────────────────────────────────
|
|
465
|
-
program
|
|
466
|
-
.command("serve [action]")
|
|
467
|
-
.description("Manage localhost HTTP vault daemon (start|stop|restart|ping)")
|
|
468
|
-
.option("--port <n>", "Port (default: 52437)")
|
|
469
|
-
.option("-p, --pw <password>", "clauth password (optional — omit to start locked, unlock in browser)")
|
|
470
|
-
.option("--services <list>", "Comma-separated service whitelist (default: all)")
|
|
471
|
-
.option("--action <action>", "Internal: action override for daemon child")
|
|
472
|
-
.addHelpText("after", `
|
|
473
|
-
Actions:
|
|
474
|
-
start Start the server as a background daemon
|
|
475
|
-
stop Stop the running daemon
|
|
476
|
-
restart Stop + start
|
|
477
|
-
ping Check if the daemon is running
|
|
478
|
-
foreground Run in foreground (Ctrl+C to stop) — default if no action given
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
clauth serve start
|
|
483
|
-
clauth serve
|
|
484
|
-
clauth serve
|
|
485
|
-
clauth serve
|
|
486
|
-
clauth serve
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cli/index.js — clauth entry point
|
|
3
|
+
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
import inquirer from "inquirer";
|
|
8
|
+
import Conf from "conf";
|
|
9
|
+
import { getConfOptions } from "./conf-path.js";
|
|
10
|
+
import { getMachineHash, deriveToken, deriveSeedHash } from "./fingerprint.js";
|
|
11
|
+
import * as api from "./api.js";
|
|
12
|
+
import os from "os";
|
|
13
|
+
|
|
14
|
+
const config = new Conf(getConfOptions());
|
|
15
|
+
const VERSION = "0.4.0";
|
|
16
|
+
|
|
17
|
+
// ============================================================
|
|
18
|
+
// Password prompt helper
|
|
19
|
+
// ============================================================
|
|
20
|
+
async function promptPassword(message = "clauth password") {
|
|
21
|
+
const { pw } = await inquirer.prompt([{
|
|
22
|
+
type: "password",
|
|
23
|
+
name: "pw",
|
|
24
|
+
message,
|
|
25
|
+
mask: "*",
|
|
26
|
+
validate: v => v.length >= 8 || "Password must be at least 8 characters"
|
|
27
|
+
}]);
|
|
28
|
+
return pw;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ============================================================
|
|
32
|
+
// Auth helper — get pw + derive token
|
|
33
|
+
// ============================================================
|
|
34
|
+
async function getAuth(pw) {
|
|
35
|
+
const password = pw || await promptPassword();
|
|
36
|
+
const machineHash = getMachineHash();
|
|
37
|
+
const { token, timestamp } = deriveToken(password, machineHash);
|
|
38
|
+
return { password, machineHash, token, timestamp };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================
|
|
42
|
+
// Program
|
|
43
|
+
// ============================================================
|
|
44
|
+
const program = new Command();
|
|
45
|
+
|
|
46
|
+
program
|
|
47
|
+
.name("clauth")
|
|
48
|
+
.version(VERSION)
|
|
49
|
+
.description(chalk.cyan("🔐 clauth") + " — Hardware-bound credential vault for LIFEAI infrastructure");
|
|
50
|
+
|
|
51
|
+
// ──────────────────────────────────────────────
|
|
52
|
+
// clauth install (Supabase provisioning + skill install + test)
|
|
53
|
+
// ──────────────────────────────────────────────
|
|
54
|
+
import { runInstall } from './commands/install.js';
|
|
55
|
+
import { runUninstall } from './commands/uninstall.js';
|
|
56
|
+
import { runScrub } from './commands/scrub.js';
|
|
57
|
+
import { runServe } from './commands/serve.js';
|
|
58
|
+
|
|
59
|
+
program
|
|
60
|
+
.command('install')
|
|
61
|
+
.description('Provision Supabase, deploy Edge Function, install Claude skill')
|
|
62
|
+
.option('--ref <ref>', 'Supabase project ref')
|
|
63
|
+
.option('--pat <pat>', 'Supabase Personal Access Token')
|
|
64
|
+
.action(async (opts) => {
|
|
65
|
+
await runInstall(opts);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
program
|
|
69
|
+
.command('uninstall')
|
|
70
|
+
.description('Full teardown — drop DB objects, Edge Function, secrets, skill, config')
|
|
71
|
+
.option('--ref <ref>', 'Supabase project ref')
|
|
72
|
+
.option('--pat <pat>', 'Supabase Personal Access Token (required)')
|
|
73
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
74
|
+
.action(async (opts) => {
|
|
75
|
+
if (!opts.yes) {
|
|
76
|
+
const inquirerMod = await import('inquirer');
|
|
77
|
+
const { confirm } = await inquirerMod.default.prompt([{
|
|
78
|
+
type: 'input',
|
|
79
|
+
name: 'confirm',
|
|
80
|
+
message: chalk.red('Type "CONFIRM UNINSTALL" to proceed:'),
|
|
81
|
+
}]);
|
|
82
|
+
if (confirm !== 'CONFIRM UNINSTALL') {
|
|
83
|
+
console.log(chalk.yellow('\n Uninstall cancelled.\n'));
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
await runUninstall(opts);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ──────────────────────────────────────────────
|
|
91
|
+
// clauth setup
|
|
92
|
+
// ──────────────────────────────────────────────
|
|
93
|
+
program
|
|
94
|
+
.command("setup")
|
|
95
|
+
.description("Register this machine with the vault (run after clauth install)")
|
|
96
|
+
.option("--admin-token <token>", "Bootstrap token (from clauth install output)")
|
|
97
|
+
.option("--label <label>", "Human label for this machine")
|
|
98
|
+
.option("-p, --pw <password>", "Password (skip interactive prompt)")
|
|
99
|
+
.action(async (opts) => {
|
|
100
|
+
console.log(chalk.cyan("\n🔐 clauth setup\n"));
|
|
101
|
+
|
|
102
|
+
// URL + anon key already saved by clauth install — fail fast if missing
|
|
103
|
+
const savedUrl = config.get("supabase_url");
|
|
104
|
+
const savedAnon = config.get("supabase_anon_key");
|
|
105
|
+
if (!savedUrl || !savedAnon) {
|
|
106
|
+
console.log(chalk.yellow(" Supabase config not found. Run clauth install first.\n"));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
console.log(chalk.gray(` Project: ${savedUrl}\n`));
|
|
110
|
+
|
|
111
|
+
let answers;
|
|
112
|
+
if (opts.pw && opts.adminToken) {
|
|
113
|
+
// Non-interactive mode — all flags provided
|
|
114
|
+
answers = { label: opts.label || os.hostname(), pw: opts.pw, adminTk: opts.adminToken };
|
|
115
|
+
} else {
|
|
116
|
+
answers = await inquirer.prompt([
|
|
117
|
+
{ type: "input", name: "label", message: "Machine label:", default: opts.label || os.hostname() },
|
|
118
|
+
{ type: "password", name: "pw", message: "Set password:", mask: "*", default: opts.pw || "" },
|
|
119
|
+
{ type: "password", name: "adminTk", message: "Bootstrap token:", mask: "*",
|
|
120
|
+
default: opts.adminToken || "" },
|
|
121
|
+
]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const spinner = ora("Registering machine with vault...").start();
|
|
125
|
+
try {
|
|
126
|
+
const machineHash = getMachineHash();
|
|
127
|
+
const seedHash = deriveSeedHash(machineHash, answers.pw);
|
|
128
|
+
const result = await api.registerMachine(machineHash, seedHash, answers.label, answers.adminTk);
|
|
129
|
+
if (result.error) throw new Error(result.error);
|
|
130
|
+
spinner.succeed(chalk.green(`Machine registered: ${machineHash.slice(0,12)}...`));
|
|
131
|
+
console.log(chalk.green("\n✓ clauth is ready.\n"));
|
|
132
|
+
console.log(chalk.cyan(" clauth test — verify connection"));
|
|
133
|
+
console.log(chalk.cyan(" clauth status — see all services\n"));
|
|
134
|
+
} catch (err) {
|
|
135
|
+
spinner.fail(chalk.red(`Setup failed: ${err.message}`));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ──────────────────────────────────────────────
|
|
141
|
+
// clauth status
|
|
142
|
+
// ──────────────────────────────────────────────
|
|
143
|
+
program
|
|
144
|
+
.command("status")
|
|
145
|
+
.description("Show all services and their state")
|
|
146
|
+
.option("-p, --pw <password>", "Password (or will prompt)")
|
|
147
|
+
.action(async (opts) => {
|
|
148
|
+
const auth = await getAuth(opts.pw);
|
|
149
|
+
const spinner = ora("Fetching service status...").start();
|
|
150
|
+
try {
|
|
151
|
+
const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp);
|
|
152
|
+
spinner.stop();
|
|
153
|
+
if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); return; }
|
|
154
|
+
|
|
155
|
+
console.log(chalk.cyan("\n🔐 clauth service status\n"));
|
|
156
|
+
console.log(
|
|
157
|
+
chalk.bold(
|
|
158
|
+
" " + "SERVICE".padEnd(20) + "TYPE".padEnd(12) + "STATUS".padEnd(12) +
|
|
159
|
+
"KEY STORED".padEnd(12) + "LAST RETRIEVED"
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
console.log(" " + "─".repeat(72));
|
|
163
|
+
|
|
164
|
+
for (const s of result.services || []) {
|
|
165
|
+
const status = s.enabled
|
|
166
|
+
? chalk.green("ACTIVE".padEnd(12))
|
|
167
|
+
: s.vault_key
|
|
168
|
+
? chalk.yellow("SUSPENDED".padEnd(12))
|
|
169
|
+
: chalk.gray("NO KEY".padEnd(12));
|
|
170
|
+
const hasKey = s.vault_key ? chalk.green("✓".padEnd(12)) : chalk.gray("—".padEnd(12));
|
|
171
|
+
const lastGet = s.last_retrieved
|
|
172
|
+
? new Date(s.last_retrieved).toLocaleDateString()
|
|
173
|
+
: chalk.gray("never");
|
|
174
|
+
|
|
175
|
+
console.log(` ${s.name.padEnd(20)}${s.key_type.padEnd(12)}${status}${hasKey}${lastGet}`);
|
|
176
|
+
}
|
|
177
|
+
console.log();
|
|
178
|
+
} catch (err) {
|
|
179
|
+
spinner.fail(chalk.red(err.message));
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ──────────────────────────────────────────────
|
|
184
|
+
// clauth write pw <new_password>
|
|
185
|
+
// clauth write params
|
|
186
|
+
// clauth write key <service> <value>
|
|
187
|
+
// ──────────────────────────────────────────────
|
|
188
|
+
const writeCmd = program.command("write").description("Write credentials or update auth parameters");
|
|
189
|
+
|
|
190
|
+
writeCmd
|
|
191
|
+
.command("pw [newpw]")
|
|
192
|
+
.description("Set or update clauth master password")
|
|
193
|
+
.action(async (newpw) => {
|
|
194
|
+
console.log(chalk.cyan("\n🔐 clauth write pw\n"));
|
|
195
|
+
const current = await promptPassword("Current password (to verify)");
|
|
196
|
+
const pw = newpw || (await inquirer.prompt([
|
|
197
|
+
{ type: "password", name: "p", message: "New password:", mask: "*" },
|
|
198
|
+
{ type: "password", name: "c", message: "Confirm new password:", mask: "*" }
|
|
199
|
+
]).then(a => { if (a.p !== a.c) { console.log(chalk.red("Passwords don't match")); process.exit(1); } return a.p; }));
|
|
200
|
+
|
|
201
|
+
// Re-register machine with new seed hash
|
|
202
|
+
const machineHash = getMachineHash();
|
|
203
|
+
const newSeedHash = deriveSeedHash(machineHash, pw);
|
|
204
|
+
const { token, timestamp } = deriveToken(current, machineHash);
|
|
205
|
+
const adminToken = await inquirer.prompt([{
|
|
206
|
+
type: "password", name: "t", message: "Admin bootstrap token (required for re-registration):", mask: "*"
|
|
207
|
+
}]).then(a => a.t);
|
|
208
|
+
|
|
209
|
+
const spinner = ora("Updating password and re-registering machine...").start();
|
|
210
|
+
try {
|
|
211
|
+
const result = await api.registerMachine(machineHash, newSeedHash, null, adminToken);
|
|
212
|
+
if (result.error) throw new Error(result.error);
|
|
213
|
+
spinner.succeed(chalk.green("Password updated and machine re-registered."));
|
|
214
|
+
} catch (err) {
|
|
215
|
+
spinner.fail(chalk.red(err.message));
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
writeCmd
|
|
220
|
+
.command("params")
|
|
221
|
+
.description("Re-read hardware fingerprint (use after hardware change)")
|
|
222
|
+
.action(async () => {
|
|
223
|
+
const spinner = ora("Reading hardware fingerprint...").start();
|
|
224
|
+
try {
|
|
225
|
+
const hash = getMachineHash();
|
|
226
|
+
spinner.succeed(chalk.green(`Machine hash: ${hash.slice(0,16)}...`));
|
|
227
|
+
console.log(chalk.gray("Full hash: " + hash));
|
|
228
|
+
} catch (err) {
|
|
229
|
+
spinner.fail(chalk.red(err.message));
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
writeCmd
|
|
234
|
+
.command("key <service> [value]")
|
|
235
|
+
.description("Write a credential into vault for a service")
|
|
236
|
+
.option("-p, --pw <password>", "Password")
|
|
237
|
+
.action(async (service, value, opts) => {
|
|
238
|
+
const auth = await getAuth(opts.pw);
|
|
239
|
+
let val = value;
|
|
240
|
+
if (!val) {
|
|
241
|
+
const { v } = await inquirer.prompt([{ type: "password", name: "v", message: `Value for ${service}:`, mask: "*" }]);
|
|
242
|
+
val = v;
|
|
243
|
+
}
|
|
244
|
+
const spinner = ora(`Writing key for ${service}...`).start();
|
|
245
|
+
try {
|
|
246
|
+
const result = await api.write(auth.password, auth.machineHash, auth.token, auth.timestamp, service, val);
|
|
247
|
+
if (result.error) throw new Error(result.error);
|
|
248
|
+
spinner.succeed(chalk.green(`Key stored in vault: auth.${service}`));
|
|
249
|
+
} catch (err) {
|
|
250
|
+
spinner.fail(chalk.red(err.message));
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ──────────────────────────────────────────────
|
|
255
|
+
// clauth enable <service|all>
|
|
256
|
+
// clauth disable <service|all>
|
|
257
|
+
// ──────────────────────────────────────────────
|
|
258
|
+
program
|
|
259
|
+
.command("enable <service>")
|
|
260
|
+
.description("Enable a service (or 'all')")
|
|
261
|
+
.option("-p, --pw <password>")
|
|
262
|
+
.action(async (service, opts) => {
|
|
263
|
+
const auth = await getAuth(opts.pw);
|
|
264
|
+
const spinner = ora(`Enabling ${service}...`).start();
|
|
265
|
+
try {
|
|
266
|
+
const result = await api.enable(auth.password, auth.machineHash, auth.token, auth.timestamp, service, true);
|
|
267
|
+
if (result.error) throw new Error(result.error);
|
|
268
|
+
spinner.succeed(chalk.green(`Enabled: ${service}`));
|
|
269
|
+
} catch (err) { spinner.fail(chalk.red(err.message)); }
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
program
|
|
273
|
+
.command("disable <service>")
|
|
274
|
+
.description("Disable a service (or 'all')")
|
|
275
|
+
.option("-p, --pw <password>")
|
|
276
|
+
.action(async (service, opts) => {
|
|
277
|
+
const auth = await getAuth(opts.pw);
|
|
278
|
+
const spinner = ora(`Disabling ${service}...`).start();
|
|
279
|
+
try {
|
|
280
|
+
const result = await api.enable(auth.password, auth.machineHash, auth.token, auth.timestamp, service, false);
|
|
281
|
+
if (result.error) throw new Error(result.error);
|
|
282
|
+
spinner.succeed(chalk.yellow(`Disabled: ${service}`));
|
|
283
|
+
} catch (err) { spinner.fail(chalk.red(err.message)); }
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ──────────────────────────────────────────────
|
|
287
|
+
// clauth add service <name>
|
|
288
|
+
// clauth remove service <name>
|
|
289
|
+
// clauth list services
|
|
290
|
+
// ──────────────────────────────────────────────
|
|
291
|
+
const addCmd = program.command("add").description("Add resources to the registry");
|
|
292
|
+
|
|
293
|
+
addCmd
|
|
294
|
+
.command("service <name>")
|
|
295
|
+
.description("Register a new service slot")
|
|
296
|
+
.option("--type <type>", "Key type: token | keypair | connstring | oauth")
|
|
297
|
+
.option("--label <label>", "Human-readable label")
|
|
298
|
+
.option("--description <desc>", "Description")
|
|
299
|
+
.option("-p, --pw <password>")
|
|
300
|
+
.action(async (name, opts) => {
|
|
301
|
+
const auth = await getAuth(opts.pw);
|
|
302
|
+
let answers;
|
|
303
|
+
if (opts.type && opts.label) {
|
|
304
|
+
// Non-interactive — all flags provided
|
|
305
|
+
answers = { label: opts.label, key_type: opts.type, desc: opts.description || "" };
|
|
306
|
+
} else {
|
|
307
|
+
answers = await inquirer.prompt([
|
|
308
|
+
{ type: "input", name: "label", message: "Label:", default: opts.label || name },
|
|
309
|
+
{ type: "list", name: "key_type", message: "Key type:", choices: ["token","keypair","connstring","oauth"], default: opts.type || "token" },
|
|
310
|
+
{ type: "input", name: "desc", message: "Description (optional):", default: opts.description || "" }
|
|
311
|
+
]);
|
|
312
|
+
}
|
|
313
|
+
const spinner = ora(`Adding service: ${name}...`).start();
|
|
314
|
+
try {
|
|
315
|
+
const result = await api.addService(
|
|
316
|
+
auth.password, auth.machineHash, auth.token, auth.timestamp,
|
|
317
|
+
name, answers.label, answers.key_type, answers.desc
|
|
318
|
+
);
|
|
319
|
+
if (result.error) throw new Error(result.error);
|
|
320
|
+
spinner.succeed(chalk.green(`Service added: ${name} (${answers.key_type})`));
|
|
321
|
+
console.log(chalk.gray(` Next: clauth write key ${name}`));
|
|
322
|
+
} catch (err) { spinner.fail(chalk.red(err.message)); }
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const removeCmd = program.command("remove").description("Remove resources from the registry");
|
|
326
|
+
|
|
327
|
+
removeCmd
|
|
328
|
+
.command("service <name>")
|
|
329
|
+
.description("Remove a service and its key from vault")
|
|
330
|
+
.option("-p, --pw <password>")
|
|
331
|
+
.action(async (name, opts) => {
|
|
332
|
+
const { confirm } = await inquirer.prompt([{
|
|
333
|
+
type: "input", name: "confirm",
|
|
334
|
+
message: chalk.red(`Type "CONFIRM REMOVE ${name.toUpperCase()}" to proceed:`)
|
|
335
|
+
}]);
|
|
336
|
+
const auth = await getAuth(opts.pw);
|
|
337
|
+
const spinner = ora(`Removing ${name}...`).start();
|
|
338
|
+
try {
|
|
339
|
+
const result = await api.removeService(auth.password, auth.machineHash, auth.token, auth.timestamp, name, confirm);
|
|
340
|
+
if (result.error) throw new Error(result.error);
|
|
341
|
+
spinner.succeed(chalk.yellow(`Removed: ${name}`));
|
|
342
|
+
} catch (err) { spinner.fail(chalk.red(err.message)); }
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
program
|
|
346
|
+
.command("list")
|
|
347
|
+
.description("List all registered services")
|
|
348
|
+
.option("-p, --pw <password>")
|
|
349
|
+
.action(async (opts) => {
|
|
350
|
+
const auth = await getAuth(opts.pw);
|
|
351
|
+
const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp);
|
|
352
|
+
if (result.error) { console.log(chalk.red(result.error)); return; }
|
|
353
|
+
console.log(chalk.cyan("\n Registered services:\n"));
|
|
354
|
+
for (const s of result.services || []) {
|
|
355
|
+
console.log(` ${chalk.bold(s.name.padEnd(20))} ${chalk.gray(s.key_type.padEnd(12))} ${chalk.gray(s.description || "")}`);
|
|
356
|
+
}
|
|
357
|
+
console.log();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// ──────────────────────────────────────────────
|
|
361
|
+
// clauth test <service|all>
|
|
362
|
+
// ──────────────────────────────────────────────
|
|
363
|
+
program
|
|
364
|
+
.command("test [service]")
|
|
365
|
+
.description("Test HMAC handshake — no key returned")
|
|
366
|
+
.option("-p, --pw <password>")
|
|
367
|
+
.action(async (service, opts) => {
|
|
368
|
+
const auth = await getAuth(opts.pw);
|
|
369
|
+
const spinner = ora("Testing auth handshake...").start();
|
|
370
|
+
try {
|
|
371
|
+
const result = await api.test(auth.password, auth.machineHash, auth.token, auth.timestamp);
|
|
372
|
+
if (result.error) throw new Error(`${result.error}: ${result.reason}`);
|
|
373
|
+
spinner.succeed(chalk.green("PASS — HMAC validated"));
|
|
374
|
+
console.log(chalk.gray(` Machine: ${auth.machineHash.slice(0,16)}...`));
|
|
375
|
+
console.log(chalk.gray(` Window: ${new Date(result.timestamp).toISOString()}`));
|
|
376
|
+
} catch (err) {
|
|
377
|
+
spinner.fail(chalk.red("FAIL — " + err.message));
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// ──────────────────────────────────────────────
|
|
382
|
+
// clauth get <service>
|
|
383
|
+
// ──────────────────────────────────────────────
|
|
384
|
+
program
|
|
385
|
+
.command("get <service>")
|
|
386
|
+
.description("Retrieve a key from vault")
|
|
387
|
+
.option("-p, --pw <password>")
|
|
388
|
+
.option("--json", "Output raw JSON")
|
|
389
|
+
.action(async (service, opts) => {
|
|
390
|
+
const auth = await getAuth(opts.pw);
|
|
391
|
+
const spinner = ora(`Retrieving ${service}...`).start();
|
|
392
|
+
try {
|
|
393
|
+
const result = await api.retrieve(auth.password, auth.machineHash, auth.token, auth.timestamp, service);
|
|
394
|
+
spinner.stop();
|
|
395
|
+
if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); return; }
|
|
396
|
+
if (opts.json) {
|
|
397
|
+
console.log(JSON.stringify(result, null, 2));
|
|
398
|
+
} else {
|
|
399
|
+
console.log(chalk.cyan(`\n🔑 ${service} (${result.key_type})\n`));
|
|
400
|
+
const val = typeof result.value === "string" ? result.value : JSON.stringify(result.value, null, 2);
|
|
401
|
+
console.log(val);
|
|
402
|
+
console.log();
|
|
403
|
+
}
|
|
404
|
+
} catch (err) {
|
|
405
|
+
spinner.fail(chalk.red(err.message));
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ──────────────────────────────────────────────
|
|
410
|
+
// clauth revoke <service|all>
|
|
411
|
+
// ──────────────────────────────────────────────
|
|
412
|
+
program
|
|
413
|
+
.command("revoke <service>")
|
|
414
|
+
.description("Delete key from vault (destructive)")
|
|
415
|
+
.option("-p, --pw <password>")
|
|
416
|
+
.action(async (service, opts) => {
|
|
417
|
+
const phrase = service === "all" ? "CONFIRM REVOKE ALL" : `CONFIRM REVOKE ${service.toUpperCase()}`;
|
|
418
|
+
const { confirm } = await inquirer.prompt([{
|
|
419
|
+
type: "input", name: "confirm",
|
|
420
|
+
message: chalk.red(`Type "${phrase}" to proceed:`)
|
|
421
|
+
}]);
|
|
422
|
+
const auth = await getAuth(opts.pw);
|
|
423
|
+
const spinner = ora(`Revoking ${service}...`).start();
|
|
424
|
+
try {
|
|
425
|
+
const result = await api.revoke(auth.password, auth.machineHash, auth.token, auth.timestamp, service, confirm);
|
|
426
|
+
if (result.error) throw new Error(result.error);
|
|
427
|
+
spinner.succeed(chalk.yellow(`Revoked: ${service}`));
|
|
428
|
+
} catch (err) { spinner.fail(chalk.red(err.message)); }
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// ──────────────────────────────────────────────
|
|
432
|
+
// clauth scrub [target]
|
|
433
|
+
// ──────────────────────────────────────────────
|
|
434
|
+
program
|
|
435
|
+
.command("scrub [target]")
|
|
436
|
+
.description("Scrub credentials from Claude Code transcript logs (no auth required)")
|
|
437
|
+
.option("--force", "Rescrub files even if already marked clean")
|
|
438
|
+
.addHelpText("after", `
|
|
439
|
+
Examples:
|
|
440
|
+
clauth scrub Scrub the most recent (active) transcript
|
|
441
|
+
clauth scrub <file> Scrub a specific .jsonl file
|
|
442
|
+
clauth scrub all Scrub every transcript in ~/.claude/projects/
|
|
443
|
+
clauth scrub all --force Rescrub all files (ignore markers)
|
|
444
|
+
`)
|
|
445
|
+
.action(async (target, opts) => {
|
|
446
|
+
await runScrub(target, opts);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// ──────────────────────────────────────────────
|
|
450
|
+
// clauth --help override banner
|
|
451
|
+
// ──────────────────────────────────────────────
|
|
452
|
+
program.addHelpText("beforeAll", chalk.cyan(`
|
|
453
|
+
██████╗██╗ █████╗ ██╗ ██╗████████╗██╗ ██╗
|
|
454
|
+
██╔════╝██║ ██╔══██╗██║ ██║╚══██╔══╝██║ ██║
|
|
455
|
+
██║ ██║ ███████║██║ ██║ ██║ ███████║
|
|
456
|
+
██║ ██║ ██╔══██║██║ ██║ ██║ ██╔══██║
|
|
457
|
+
╚██████╗███████╗██║ ██║╚██████╔╝ ██║ ██║ ██║
|
|
458
|
+
╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝
|
|
459
|
+
v${VERSION} — LIFEAI Credential Vault
|
|
460
|
+
`));
|
|
461
|
+
|
|
462
|
+
// ──────────────────────────────────────────────
|
|
463
|
+
// clauth serve [action]
|
|
464
|
+
// ──────────────────────────────────────────────
|
|
465
|
+
program
|
|
466
|
+
.command("serve [action]")
|
|
467
|
+
.description("Manage localhost HTTP vault daemon (start|stop|restart|ping)")
|
|
468
|
+
.option("--port <n>", "Port (default: 52437)")
|
|
469
|
+
.option("-p, --pw <password>", "clauth password (optional — omit to start locked, unlock in browser)")
|
|
470
|
+
.option("--services <list>", "Comma-separated service whitelist (default: all)")
|
|
471
|
+
.option("--action <action>", "Internal: action override for daemon child")
|
|
472
|
+
.addHelpText("after", `
|
|
473
|
+
Actions:
|
|
474
|
+
start Start the server as a background daemon
|
|
475
|
+
stop Stop the running daemon
|
|
476
|
+
restart Stop + start
|
|
477
|
+
ping Check if the daemon is running
|
|
478
|
+
foreground Run in foreground (Ctrl+C to stop) — default if no action given
|
|
479
|
+
mcp Run as MCP stdio server for Claude Code (JSON-RPC over stdin/stdout)
|
|
480
|
+
|
|
481
|
+
Examples:
|
|
482
|
+
clauth serve start Start locked — unlock at http://127.0.0.1:52437
|
|
483
|
+
clauth serve start -p mypass Start pre-unlocked (password in memory only)
|
|
484
|
+
clauth serve stop Stop the daemon
|
|
485
|
+
clauth serve ping Check status
|
|
486
|
+
clauth serve restart Restart (stays locked until browser unlock)
|
|
487
|
+
clauth serve start --services github,vercel
|
|
488
|
+
clauth serve mcp Start MCP server for Claude Code
|
|
489
|
+
clauth serve mcp -p mypass Start MCP server pre-unlocked
|
|
490
|
+
`)
|
|
491
|
+
.action(async (action, opts) => {
|
|
492
|
+
const resolvedAction = opts.action || action || "foreground";
|
|
493
|
+
await runServe({ ...opts, action: resolvedAction });
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
program.parse(process.argv);
|