@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.
- package/cli/commands/serve.js +44 -31
- package/cli/enrollment-script.js +33 -13
- package/cli/index.js +3 -2
- package/package.json +2 -2
package/cli/commands/serve.js
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
-
|
|
12879
|
-
|
|
12880
|
-
|
|
12881
|
-
|
|
12882
|
-
|
|
12883
|
-
|
|
12884
|
-
|
|
12885
|
-
|
|
12886
|
-
|
|
12887
|
-
|
|
12888
|
-
}
|
|
12889
|
-
|
|
12890
|
-
|
|
12891
|
-
|
|
12892
|
-
|
|
12893
|
-
|
|
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
|
|
package/cli/enrollment-script.js
CHANGED
|
@@ -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
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
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.
|
|
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",
|