@lifeaitools/clauth 1.30.4 → 1.30.6

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.
@@ -627,8 +627,9 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
627
627
  .btn-reveal{background:#1e3a5f;color:#60a5fa}.btn-reveal:hover{background:#1e4a7f}
628
628
  .btn-copy{background:#1a3328;color:#4ade80}.btn-copy:hover{background:#1a4338}
629
629
  .btn-set{background:#2d1f4a;color:#a78bfa}.btn-set:hover{background:#3d2f5a}
630
- .btn-save{background:#1e3a5f;color:#60a5fa;padding:6px 16px}.btn-save:hover{background:#1e4a7f}
631
- .btn-cancel{background:transparent;color:#64748b;padding:6px 10px}.btn-cancel:hover{color:#94a3b8}
630
+ .btn-save{background:#1e3a5f;color:#60a5fa;padding:6px 16px}.btn-save:hover{background:#1e4a7f}
631
+ .btn-generate{background:#3b2f0d;color:#facc15;padding:6px 12px}.btn-generate:hover{background:#56420d}
632
+ .btn-cancel{background:transparent;color:#64748b;padding:6px 10px}.btn-cancel:hover{color:#94a3b8}
632
633
  .btn-check{background:#0f2d2d;color:#34d399;border:1px solid #064e3b;padding:7px 16px;font-size:.85rem;border-radius:7px;cursor:pointer;font-weight:500;transition:all .15s}
633
634
  .btn-check:hover{background:#134e4a;border-color:#34d399}.btn-check:disabled{opacity:.5;cursor:not-allowed}
634
635
  .btn-enable{background:#14291a;color:#4ade80;border:1px solid #166534}.btn-enable:hover{background:#1a3d22;border-color:#4ade80}
@@ -1198,10 +1199,11 @@ function renderSetPanel(serviceOrName) {
1198
1199
  return \`
1199
1200
  <div class="set-panel" id="set-panel-\${id}">
1200
1201
  <label>New value for <strong>\${displayName}</strong> — paste here, never in chat</label>
1201
- <textarea class="set-input" id="set-input-\${id}" placeholder="\${htmlEscape(SERVICE_HINTS[name] || "Paste credential…")}" spellcheck="false"></textarea>
1202
- \${SERVICE_HINTS[name] ? \`<div style="font-size:.72rem;color:#475569;margin-top:4px;font-family:'Courier New',monospace">\${htmlEscape(SERVICE_HINTS[name])}</div>\` : ""}
1203
- <div class="set-foot">
1204
- <button class="btn btn-save" type="button" onclick='saveKey(\${arg})'>Save</button>
1202
+ <textarea class="set-input" id="set-input-\${id}" placeholder="\${htmlEscape(SERVICE_HINTS[name] || "Paste credential…")}" spellcheck="false"></textarea>
1203
+ \${SERVICE_HINTS[name] ? \`<div style="font-size:.72rem;color:#475569;margin-top:4px;font-family:'Courier New',monospace">\${htmlEscape(SERVICE_HINTS[name])}</div>\` : ""}
1204
+ <div class="set-foot">
1205
+ <button class="btn btn-generate" type="button" onclick='generateSecretKey(\${arg})'>Generate 32-byte key</button>
1206
+ <button class="btn btn-save" type="button" onclick='saveKey(\${arg})'>Save</button>
1205
1207
  <button class="btn btn-cancel" type="button" onclick='toggleSet(\${arg})'>Cancel</button>
1206
1208
  <span class="set-msg" id="set-msg-\${id}"></span>
1207
1209
  </div>
@@ -2026,10 +2028,19 @@ function toggleSet(name) {
2026
2028
  }
2027
2029
  }
2028
2030
 
2029
- function getServiceKeyType(name) {
2030
- const svc = allServices.find(s => s.name === name);
2031
- return String(svc?.key_type || "").toLowerCase();
2032
- }
2031
+ function getServiceKeyType(name) {
2032
+ const svc = allServices.find(s => s.name === name);
2033
+ return String(svc?.key_type || "").toLowerCase();
2034
+ }
2035
+
2036
+ function generateSecretKey(name) {
2037
+ const input = document.getElementById("set-input-" + serviceDomId(name));
2038
+ if (!input) return;
2039
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
2040
+ input.value = Array.from(bytes, byte => byte.toString(16).padStart(2, "0")).join("");
2041
+ input.focus();
2042
+ input.select();
2043
+ }
2033
2044
 
2034
2045
  async function saveKey(name) {
2035
2046
  const id = serviceDomId(name);
@@ -12610,14 +12621,14 @@ async function actionInstall(opts) {
12610
12621
  // 2. No password → install watchdog that starts daemon in locked mode
12611
12622
  // The browser dashboard opens, user enters password there.
12612
12623
  // For passwordless restart, user can later run: clauth serve seal
12613
- const pw = opts.pw || null;
12624
+ const pw = opts.pw || (opts.pwStdin ? fs.readFileSync(0, "utf8").replace(/\r?\n$/, "") : null);
12614
12625
 
12615
12626
  if (platform === "win32") {
12616
12627
  await installWindows(pw, tunnelHostname, execSync);
12617
12628
  } else if (platform === "darwin") {
12618
12629
  await installMacOS(pw, tunnelHostname, execSync);
12619
12630
  } else {
12620
- await installLinux(pw, tunnelHostname, execSync);
12631
+ await installLinux(pw, tunnelHostname, execSync, opts.pwStdin === true);
12621
12632
  }
12622
12633
  }
12623
12634
 
@@ -12851,7 +12862,7 @@ async function installMacOS(pw, tunnelHostname, execSync) {
12851
12862
  }
12852
12863
 
12853
12864
  // ── Linux: encrypted file + systemd user service ───────────
12854
- async function installLinux(pw, tunnelHostname, execSync) {
12865
+ async function installLinux(pw, tunnelHostname, execSync, forceBootKey = false) {
12855
12866
  const configDir = path.join(os.homedir(), ".config", "clauth");
12856
12867
  const bootKeyPath = path.join(configDir, "boot.key");
12857
12868
  const systemdDir = path.join(os.homedir(), ".config", "systemd", "user");
@@ -12864,7 +12875,7 @@ async function installLinux(pw, tunnelHostname, execSync) {
12864
12875
  let useSecretTool = false;
12865
12876
  const spinner = ora("Storing password securely...").start();
12866
12877
 
12867
- if (pw) {
12878
+ if (pw && !forceBootKey) {
12868
12879
  try {
12869
12880
  execSync("which secret-tool", { encoding: "utf8", stdio: "pipe" });
12870
12881
  execFileSync("secret-tool", ["store", "--label=clauth daemon password", "service", "clauth", "account", "daemon"], {
@@ -12874,23 +12885,25 @@ async function installLinux(pw, tunnelHostname, execSync) {
12874
12885
  });
12875
12886
  useSecretTool = true;
12876
12887
  spinner.succeed(chalk.green("Password stored via secret-tool (GNOME Keyring)"));
12877
- } catch {
12878
- // Fallback: encrypt with openssl and store in file.
12879
- try {
12880
- const machineId = fs.readFileSync("/etc/machine-id", "utf8").trim();
12881
- const encrypted = execFileSync("openssl", ["enc", "-aes-256-cbc", "-pbkdf2", "-iter", "100000", "-pass", `pass:${machineId}`, "-base64"], {
12882
- input: pw,
12883
- encoding: "utf8",
12884
- stdio: ["pipe", "pipe", "pipe"],
12885
- }).trim();
12886
- fs.writeFileSync(bootKeyPath, encrypted, { mode: 0o600 });
12887
- spinner.succeed(chalk.green("Password encrypted -> boot.key (openssl fallback)"));
12888
- } catch (err) {
12889
- spinner.fail(chalk.red(`Password storage failed: ${err.message}`));
12890
- process.exit(1);
12891
- }
12892
- }
12893
- } else {
12888
+ } catch {}
12889
+ }
12890
+
12891
+ if (pw && !useSecretTool) {
12892
+ // A desktop keyring cannot reliably unlock for a headless systemd service.
12893
+ try {
12894
+ const machineId = fs.readFileSync("/etc/machine-id", "utf8").trim();
12895
+ const encrypted = execFileSync("openssl", ["enc", "-aes-256-cbc", "-pbkdf2", "-iter", "100000", "-pass", `pass:${machineId}`, "-base64"], {
12896
+ input: pw,
12897
+ encoding: "utf8",
12898
+ stdio: ["pipe", "pipe", "pipe"],
12899
+ }).trim();
12900
+ fs.writeFileSync(bootKeyPath, encrypted, { mode: 0o600 });
12901
+ spinner.succeed(chalk.green(`Password encrypted -> boot.key (${forceBootKey ? "headless mode" : "openssl fallback"})`));
12902
+ } catch (err) {
12903
+ spinner.fail(chalk.red(`Password storage failed: ${err.message}`));
12904
+ process.exit(1);
12905
+ }
12906
+ } else if (!pw) {
12894
12907
  spinner.succeed(chalk.yellow("No password supplied; daemon will start locked"));
12895
12908
  }
12896
12909
 
@@ -32,20 +32,40 @@ function windowsScript({ supabaseUrl, anonKey, enrollmentCode }) {
32
32
  ].join("\r\n");
33
33
  }
34
34
 
35
- function linuxScript({ supabaseUrl, anonKey, enrollmentCode }) {
36
- return [
37
- "#!/usr/bin/env sh",
38
- "set -eu",
39
- "label=$(hostname)",
40
- "command -v node >/dev/null || { echo 'Node.js 18+ is required.' >&2; exit 1; }",
41
- "command -v npm >/dev/null || { echo 'npm is required.' >&2; exit 1; }",
42
- "npm install -g @lifeaitools/clauth@latest",
43
- ["clauth setup", `--supabase-url ${posixShellQuote(supabaseUrl)}`, `--anon-key ${posixShellQuote(anonKey)}`, `--enrollment-code ${posixShellQuote(enrollmentCode)}`, '--label "$label"'].join(" "),
44
- "clauth serve install",
45
- "echo 'For reboot persistence on a headless host, run: loginctl enable-linger \"$USER\"'",
35
+ function linuxScript({ supabaseUrl, anonKey, enrollmentCode }) {
36
+ return [
37
+ "#!/usr/bin/env sh",
38
+ "set -eu",
39
+ "label=$(hostname)",
40
+ "as_root() { if [ \"$(id -u)\" -eq 0 ]; then \"$@\"; elif command -v sudo >/dev/null; then sudo \"$@\"; else echo 'Root or sudo is required to install prerequisites.' >&2; exit 1; fi; }",
41
+ "install_prerequisites() {",
42
+ " if command -v apt-get >/dev/null; then as_root apt-get update; as_root apt-get install -y nodejs npm openssl;",
43
+ " elif command -v dnf >/dev/null; then as_root dnf install -y nodejs npm openssl;",
44
+ " else echo 'Headless enrollment supports systemd hosts with apt-get or dnf. Install Node.js 18+, npm, and openssl, then rerun this script.' >&2; exit 1; fi",
45
+ "}",
46
+ "if ! command -v systemctl >/dev/null || ! command -v loginctl >/dev/null; then echo 'Headless enrollment requires systemd and loginctl.' >&2; exit 1; fi",
47
+ "if ! command -v node >/dev/null || ! command -v npm >/dev/null || ! command -v openssl >/dev/null; then install_prerequisites; fi",
48
+ "node_major=$(node -p \"process.versions.node.split('.')[0]\")",
49
+ "if [ \"$node_major\" -lt 18 ]; then echo 'Node.js 18+ is required.' >&2; exit 1; fi",
50
+ "if ! npm install -g @lifeaitools/clauth@latest; then as_root npm install -g @lifeaitools/clauth@latest; fi",
51
+ ["clauth setup", `--supabase-url ${posixShellQuote(supabaseUrl)}`, `--anon-key ${posixShellQuote(anonKey)}`, `--enrollment-code ${posixShellQuote(enrollmentCode)}`, '--label "$label"'].join(" "),
52
+ "user_name=$(id -un)",
53
+ "if loginctl enable-linger \"$user_name\"; then echo \"Linger enabled for $user_name.\"; elif as_root loginctl enable-linger \"$user_name\"; then echo \"Linger enabled for $user_name.\"; else echo 'Could not enable linger for unattended restart.' >&2; exit 1; fi",
54
+ "if ! test -t 0; then echo 'Headless enrollment requires an interactive TTY to set the vault password.' >&2; exit 1; fi",
55
+ "restore_echo() { stty echo 2>/dev/null || true; }",
56
+ "trap restore_echo EXIT HUP INT TERM",
57
+ "printf 'Re-enter the vault password to enable unattended restart: ' >&2",
58
+ "stty -echo",
59
+ "IFS= read -r vault_password",
60
+ "stty echo",
61
+ "trap - EXIT HUP INT TERM",
62
+ "printf '\\n' >&2",
63
+ "[ -n \"$vault_password\" ] || { echo 'A vault password is required for unattended restart.' >&2; exit 1; }",
64
+ "printf %s \"$vault_password\" | clauth serve install --pw-stdin",
65
+ "unset vault_password",
46
66
  "rm -f -- \"$0\"",
47
- ].join("\n");
48
- }
67
+ ].join("\n");
68
+ }
49
69
 
50
70
  export function writeEnrollmentScript({ supabaseUrl, anonKey, enrollmentCode, label, target = "windows", appDir } = {}) {
51
71
  if (!["windows", "linux"].includes(target)) throw new Error(`Unsupported enrollment target: ${target}`);
package/cli/index.js CHANGED
@@ -1007,8 +1007,9 @@ program
1007
1007
  .option("--tunnel <hostname>", "Fixed tunnel hostname (e.g. clauth.prtrust.fund) — uses named Cloudflare Tunnel instead of random URL")
1008
1008
  .option("--staged", "Start on staging port (52438) for blue-green verification before make-live")
1009
1009
  .option("--isolated", "Run on a non-live port without touching live PID files, browser, or boot-key credentials")
1010
- .option("--from-boot-key", "Internal: password came from boot.key auto-unlock (degrade gracefully on verify failure)")
1011
- .option("--pw-env", "Internal: read the daemon password from CLAUTH_BOOT_PASSWORD")
1010
+ .option("--from-boot-key", "Internal: password came from boot.key auto-unlock (degrade gracefully on verify failure)")
1011
+ .option("--pw-env", "Internal: read the daemon password from CLAUTH_BOOT_PASSWORD")
1012
+ .option("--pw-stdin", "Internal: read the installer password from standard input")
1012
1013
  .option("--action <action>", "Internal: action override for daemon child")
1013
1014
  .addHelpText("after", `
1014
1015
  Actions:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "1.30.4",
3
+ "version": "1.30.6",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "build": "bash scripts/build.sh",
11
- "test": "node --test cli/commands/scrub.test.js cli/api.classify.test.js test/studio-debug.test.mjs && node test-auth-verdict.mjs",
11
+ "test": "node --test cli/commands/scrub.test.js cli/api.classify.test.js test/studio-debug.test.mjs test/dashboard-key-generator.test.mjs && node test-auth-verdict.mjs",
12
12
  "test:studio-debug": "node --test test/studio-debug.test.mjs",
13
13
  "test:agent-pool": "node test/agent-pool.test.mjs",
14
14
  "test:call-agent-10": "node test/call-agent-10-skills.test.mjs",