@lifeaitools/clauth 0.7.6 → 1.2.3

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/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 = "0.7.0";
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
- console.log(chalk.cyan("\n🔐 clauth service status\n"));
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(20) + "TYPE".padEnd(12) + "STATUS".padEnd(12) +
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(72));
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(20)}${s.key_type.padEnd(12)}${status}${hasKey}${lastGet}`);
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
- console.log(chalk.cyan("\n Registered services:\n"));
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
- console.log(` ${chalk.bold(s.name.padEnd(20))} ${chalk.gray(s.key_type.padEnd(12))} ${chalk.gray(s.description || "")}`);
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,166 @@ 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 start|stop|status
536
+ // (setup moved to in-browser wizard at http://127.0.0.1:52437)
537
+ // ──────────────────────────────────────────────
538
+ const tunnelCmd = program.command("tunnel").description("Manage Cloudflare tunnel for claude.ai web integration");
539
+
540
+ tunnelCmd
541
+ .command("setup")
542
+ .description("Open the tunnel setup wizard in your browser")
543
+ .action(async () => {
544
+ console.log(chalk.cyan("\n Tunnel setup is now handled in the browser.\n"));
545
+ console.log(chalk.white(" 1. Start the daemon: clauth serve start"));
546
+ console.log(chalk.white(" 2. Open: http://127.0.0.1:52437"));
547
+ console.log(chalk.white(" 3. Unlock the vault and click \"Setup Tunnel\"\n"));
548
+ });
549
+
550
+ tunnelCmd
551
+ .command("start")
552
+ .description("Tell daemon to start the tunnel")
553
+ .action(async () => {
554
+ try {
555
+ const r = await fetch("http://127.0.0.1:52437/tunnel/start", {
556
+ method: "POST",
557
+ headers: { "Content-Type": "application/json" },
558
+ signal: AbortSignal.timeout(5000),
559
+ });
560
+ const data = await r.json().catch(() => ({}));
561
+ if (!r.ok) {
562
+ console.error(` ✗ ${data.error || r.statusText}`);
563
+ if (r.status === 401) console.error(" Unlock the daemon first: http://127.0.0.1:52437");
564
+ process.exit(1);
565
+ }
566
+ console.log(` ✓ ${data.message || "Tunnel starting — check status with: clauth tunnel status"}`);
567
+ } catch (e) {
568
+ console.error(" ✗ Daemon not running. Start it with: clauth serve");
569
+ process.exit(1);
570
+ }
571
+ });
572
+
573
+ tunnelCmd
574
+ .command("stop")
575
+ .description("Tell daemon to stop the tunnel")
576
+ .action(async () => {
577
+ try {
578
+ const r = await fetch("http://127.0.0.1:52437/tunnel/stop", {
579
+ method: "POST",
580
+ headers: { "Content-Type": "application/json" },
581
+ signal: AbortSignal.timeout(5000),
582
+ });
583
+ const data = await r.json().catch(() => ({}));
584
+ if (!r.ok) {
585
+ console.error(` ✗ ${data.error || r.statusText}`);
586
+ process.exit(1);
587
+ }
588
+ console.log(" ✓ Tunnel stopped.");
589
+ } catch (e) {
590
+ console.error(" ✗ Daemon not running.");
591
+ process.exit(1);
592
+ }
593
+ });
594
+
595
+ tunnelCmd
596
+ .command("status")
597
+ .description("Show current tunnel status")
598
+ .action(async () => {
599
+ try {
600
+ const r = await fetch("http://127.0.0.1:52437/tunnel", {
601
+ signal: AbortSignal.timeout(5000),
602
+ });
603
+ const data = await r.json().catch(() => ({}));
604
+ const icons = {
605
+ live: "✓", starting: "◌", not_configured: "⚠",
606
+ not_started: "○", error: "✗", missing_cloudflared: "✗",
607
+ };
608
+ const labels = {
609
+ live: `Live — ${data.url || ""}`,
610
+ starting: "Starting...",
611
+ not_configured: "Not configured — open http://127.0.0.1:52437 and click Setup Tunnel",
612
+ not_started: "Not started — run: clauth tunnel start",
613
+ error: `Error${data.error ? ": " + data.error : ""} — check cloudflared config`,
614
+ missing_cloudflared: "cloudflared not installed — https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/",
615
+ };
616
+ const status = data.status || "unknown";
617
+ console.log(`\n ${icons[status] || "?"} Tunnel: ${labels[status] || status}\n`);
618
+ } catch (e) {
619
+ console.error(" ✗ Daemon not running. Start it with: clauth serve");
620
+ process.exit(1);
621
+ }
622
+ });
623
+
449
624
  // ──────────────────────────────────────────────
450
625
  // clauth --help override banner
451
626
  // ──────────────────────────────────────────────
@@ -478,8 +653,9 @@ Actions:
478
653
  ping Check if the daemon is running
479
654
  foreground Run in foreground (Ctrl+C to stop) — default if no action given
480
655
  mcp Run as MCP stdio server for Claude Code (JSON-RPC over stdin/stdout)
481
- install (Windows) Encrypt password with DPAPI + create Scheduled Task for auto-start on login
482
- uninstall (Windows) Remove Scheduled Task + delete boot.key
656
+ install Store password securely + register auto-start service (cross-platform)
657
+ Windows: DPAPI + Scheduled Task | macOS: Keychain + LaunchAgent | Linux: libsecret/openssl + systemd
658
+ uninstall Remove auto-start service + delete stored password
483
659
 
484
660
  MCP SSE (built into start/foreground):
485
661
  The HTTP daemon also serves MCP SSE transport at GET /sse + POST /message.
@@ -494,8 +670,9 @@ Examples:
494
670
  clauth serve start --services github,vercel
495
671
  clauth serve mcp Start MCP server for Claude Code
496
672
  clauth serve mcp -p mypass Start MCP server pre-unlocked
497
- clauth serve install (Windows) Set up auto-start on login via DPAPI
498
- clauth serve uninstall (Windows) Remove auto-start
673
+ clauth serve install Set up auto-start on login (DPAPI/Keychain/libsecret)
674
+ clauth serve install --tunnel host Auto-start with Cloudflare Tunnel
675
+ clauth serve uninstall Remove auto-start
499
676
  `)
500
677
  .action(async (action, opts) => {
501
678
  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": "0.7.6",
3
+ "version": "1.2.3",
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",
@@ -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(() => {});
@@ -119,12 +119,27 @@ async function handleEnable(sb: any, body: any, mh: string) {
119
119
  }
120
120
 
121
121
  async function handleAdd(sb: any, body: any, mh: string) {
122
- const { name, label, key_type, description } = body;
122
+ const { name, label, key_type, description, project } = body;
123
123
  if (!name || !label || !key_type) return { error: "name, label, key_type required" };
124
- const { error } = await sb.from("clauth_services").insert({ name, label, key_type, description: description || null });
124
+ const row: any = { name, label, key_type, description: description || null };
125
+ if (project) row.project = project;
126
+ const { error } = await sb.from("clauth_services").insert(row);
125
127
  if (error) return { error: error.message };
126
128
  await auditLog(sb, mh, name, "add", "success");
127
- return { success: true, name, label, key_type };
129
+ return { success: true, name, label, key_type, project: project || null };
130
+ }
131
+
132
+ async function handleUpdate(sb: any, body: any, mh: string) {
133
+ const { service, project, label, description } = body;
134
+ if (!service) return { error: "service required" };
135
+ const updates: any = { updated_at: new Date().toISOString() };
136
+ if (project !== undefined) updates.project = project || null; // empty string clears project
137
+ if (label !== undefined) updates.label = label;
138
+ if (description !== undefined) updates.description = description || null;
139
+ const { error } = await sb.from("clauth_services").update(updates).eq("name", service);
140
+ if (error) return { error: error.message };
141
+ await auditLog(sb, mh, service, "update", "success", `fields: ${Object.keys(updates).join(", ")}`);
142
+ return { success: true, service, ...updates };
128
143
  }
129
144
 
130
145
  async function handleRemove(sb: any, body: any, mh: string) {
@@ -152,9 +167,13 @@ async function handleRevoke(sb: any, body: any, mh: string) {
152
167
  return { success: true, service };
153
168
  }
154
169
 
155
- async function handleStatus(sb: any, mh: string) {
156
- const { data: services } = await sb.from("clauth_services")
157
- .select("name, label, key_type, enabled, vault_key, last_retrieved, last_rotated, created_at").order("name");
170
+ async function handleStatus(sb: any, body: any, mh: string) {
171
+ let q = sb.from("clauth_services")
172
+ .select("name, label, key_type, enabled, vault_key, last_retrieved, last_rotated, created_at, project")
173
+ .order("project", { ascending: true, nullsFirst: true })
174
+ .order("name");
175
+ if (body.project) q = q.eq("project", body.project);
176
+ const { data: services } = await q;
158
177
  await auditLog(sb, mh, "all", "status", "success");
159
178
  return { services: services || [] };
160
179
  }
@@ -225,9 +244,10 @@ Deno.serve(async (req: Request) => {
225
244
  case "write": return Response.json(await handleWrite(sb, body, mh));
226
245
  case "enable": return Response.json(await handleEnable(sb, body, mh));
227
246
  case "add": return Response.json(await handleAdd(sb, body, mh));
247
+ case "update": return Response.json(await handleUpdate(sb, body, mh));
228
248
  case "remove": return Response.json(await handleRemove(sb, body, mh));
229
249
  case "revoke": return Response.json(await handleRevoke(sb, body, mh));
230
- case "status": return Response.json(await handleStatus(sb, mh));
250
+ case "status": return Response.json(await handleStatus(sb, body, mh));
231
251
  case "change-password": return Response.json(await handleChangePassword(sb, body, mh));
232
252
  case "test": return Response.json({ valid: true, machine_hash: mh, timestamp: body.timestamp, ip });
233
253
  default: return Response.json({ error: "unknown_route", route }, { status: 404 });
@@ -0,0 +1,13 @@
1
+ -- clauth_config: key/value store for daemon configuration
2
+ -- Used to persist tunnel hostname and other daemon settings
3
+ CREATE TABLE IF NOT EXISTS clauth_config (
4
+ key text PRIMARY KEY,
5
+ value jsonb NOT NULL,
6
+ updated_at timestamptz DEFAULT now()
7
+ );
8
+
9
+ -- RLS: only service role can access (same pattern as other clauth tables)
10
+ ALTER TABLE clauth_config ENABLE ROW LEVEL SECURITY;
11
+
12
+ -- Seed: no tunnel by default (user must run clauth tunnel setup)
13
+ -- INSERT INTO clauth_config (key, value) VALUES ('tunnel_hostname', 'null');