@lifeaitools/clauth 1.4.2 → 1.4.4

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.
@@ -959,20 +959,35 @@ async function boot() {
959
959
  try {
960
960
  const ping = await fetch(BASE + "/ping").then(r => r.json());
961
961
  if (ping.locked) {
962
- showLockScreen();
962
+ showLockScreen(ping.hard_locked);
963
963
  } else {
964
964
  showMain(ping);
965
965
  loadServices();
966
966
  }
967
967
  } catch {
968
- showLockScreen();
968
+ showLockScreen(false);
969
969
  }
970
970
  }
971
971
 
972
- function showLockScreen() {
972
+ function showLockScreen(hardLocked) {
973
973
  document.getElementById("lock-screen").style.display = "flex";
974
974
  document.getElementById("main-view").style.display = "none";
975
- setTimeout(() => document.getElementById("lock-input").focus(), 50);
975
+ const input = document.getElementById("lock-input");
976
+ const btn = document.getElementById("unlock-btn");
977
+ const sub = document.getElementById("lock-sub");
978
+ const err = document.getElementById("lock-err");
979
+ if (hardLocked) {
980
+ input.disabled = true;
981
+ btn.disabled = true;
982
+ sub.textContent = "Too many failed attempts";
983
+ err.textContent = "✗ Vault locked — restart daemon to try again";
984
+ } else {
985
+ input.disabled = false;
986
+ btn.disabled = false;
987
+ sub.textContent = "Paste your password to unlock";
988
+ err.textContent = "";
989
+ setTimeout(() => input.focus(), 50);
990
+ }
976
991
  }
977
992
 
978
993
  // ── Upgrade detection ────────────────────────────────────────────
@@ -1011,6 +1026,11 @@ function showMain(ping) {
1011
1026
  checkUpgrade(ping);
1012
1027
  document.getElementById("lock-screen").style.display = "none";
1013
1028
  document.getElementById("main-view").style.display = "block";
1029
+ // Reset lock screen state for next lock
1030
+ document.getElementById("lock-input").disabled = false;
1031
+ document.getElementById("unlock-btn").disabled = false;
1032
+ document.getElementById("lock-sub").textContent = "Paste your password to unlock";
1033
+ document.getElementById("lock-err").textContent = "";
1014
1034
  if (ping) {
1015
1035
  document.getElementById("s-pid").textContent = ping.pid || "—";
1016
1036
  document.getElementById("s-fails").textContent =
@@ -1025,8 +1045,10 @@ async function unlock() {
1025
1045
  const input = document.getElementById("lock-input");
1026
1046
  const btn = document.getElementById("unlock-btn");
1027
1047
  const err = document.getElementById("lock-err");
1048
+ const sub = document.getElementById("lock-sub");
1028
1049
  const pw = input.value;
1029
1050
 
1051
+ if (input.disabled) return;
1030
1052
  if (!pw) { err.textContent = "Password is required."; return; }
1031
1053
 
1032
1054
  btn.disabled = true; btn.textContent = "Verifying…"; err.textContent = "";
@@ -1038,7 +1060,22 @@ async function unlock() {
1038
1060
  body: JSON.stringify({ password: pw })
1039
1061
  }).then(r => r.json());
1040
1062
 
1041
- if (r.error) throw new Error(r.error);
1063
+ if (r.hard_locked) {
1064
+ input.value = "";
1065
+ input.disabled = true;
1066
+ btn.disabled = true;
1067
+ btn.textContent = "Unlock";
1068
+ sub.textContent = "Too many failed attempts";
1069
+ err.textContent = "✗ Vault locked — restart daemon to try again";
1070
+ return;
1071
+ }
1072
+
1073
+ if (r.error) {
1074
+ const rem = r.failures_remaining !== undefined
1075
+ ? \` (\${r.failures_remaining} attempt\${r.failures_remaining !== 1 ? "s" : ""} remaining)\`
1076
+ : "";
1077
+ throw new Error(r.error + rem);
1078
+ }
1042
1079
 
1043
1080
  input.value = "";
1044
1081
  const ping = await fetch(BASE + "/ping").then(r => r.json());
@@ -1050,14 +1087,14 @@ async function unlock() {
1050
1087
  err.textContent = "✗ " + (e.message || "Invalid password");
1051
1088
  setTimeout(() => input.className = "lock-input", 600);
1052
1089
  } finally {
1053
- btn.disabled = false; btn.textContent = "Unlock";
1090
+ if (!input.disabled) { btn.disabled = false; btn.textContent = "Unlock"; }
1054
1091
  }
1055
1092
  }
1056
1093
 
1057
1094
  // ── Lock ────────────────────────────────────
1058
1095
  async function lockVault() {
1059
- await fetch(BASE + "/lock", { method: "POST" }).catch(() => {});
1060
- showLockScreen();
1096
+ const r = await fetch(BASE + "/lock", { method: "POST" }).then(r => r.json()).catch(() => ({}));
1097
+ showLockScreen(r.hard_locked || false);
1061
1098
  }
1062
1099
 
1063
1100
  // ── Make Live (blue-green: promote staged instance to live port) ──
@@ -2279,6 +2316,9 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2279
2316
  }
2280
2317
  const MAX_FAILS = 10;
2281
2318
  let failCount = 0;
2319
+ const MAX_AUTH_FAILS = 10;
2320
+ let authFailCount = 0;
2321
+ let authHardLocked = false;
2282
2322
  let password = initPassword || null; // null = locked; set via POST /auth
2283
2323
  const machineHash = getMachineHash();
2284
2324
 
@@ -2487,17 +2527,18 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2487
2527
  error: message,
2488
2528
  failures: failCount,
2489
2529
  failures_remaining: remaining,
2490
- ...(failCount >= MAX_FAILS ? { shutdown: true } : {})
2530
+ ...(failCount >= MAX_FAILS ? { locked: true } : {})
2491
2531
  });
2492
2532
 
2493
2533
  res.writeHead(code, { "Content-Type": "application/json", ...CORS });
2494
2534
  res.end(body);
2495
2535
 
2496
2536
  if (failCount >= MAX_FAILS) {
2497
- const msg = `[${new Date().toISOString()}] Failure limit reached — shutting down\n`;
2537
+ const msg = `[${new Date().toISOString()}] Failure limit reached — locking vault\n`;
2498
2538
  try { fs.appendFileSync(LOG_FILE, msg); } catch {}
2499
- removePid();
2500
- setTimeout(() => process.exit(1), 100);
2539
+ password = null;
2540
+ failCount = 0;
2541
+ stopTunnel();
2501
2542
  }
2502
2543
  }
2503
2544
 
@@ -3013,8 +3054,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3013
3054
  status: "ok",
3014
3055
  pid: process.pid,
3015
3056
  locked: !password,
3057
+ hard_locked: authHardLocked,
3016
3058
  failures: failCount,
3017
3059
  failures_remaining: MAX_FAILS - failCount,
3060
+ auth_failures: authFailCount,
3061
+ auth_failures_remaining: MAX_AUTH_FAILS - authFailCount,
3018
3062
  services: whitelist || "all",
3019
3063
  port,
3020
3064
  tunnel_status: tunnelStatus,
@@ -3092,8 +3136,9 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3092
3136
  return ok(res, { status: tunnelStatus });
3093
3137
  }
3094
3138
 
3095
- // GET /shutdown (for daemon stop — programmatic, keeps boot.key)
3096
- if (method === "GET" && reqPath === "/shutdown") {
3139
+ // GET|POST /shutdown (for daemon stop — programmatic, keeps boot.key)
3140
+ // Accept POST as well older scripts and curl default to POST
3141
+ if ((method === "GET" || method === "POST") && reqPath === "/shutdown") {
3097
3142
  stopTunnel();
3098
3143
  ok(res, { ok: true, message: "shutting down" });
3099
3144
  removePid();
@@ -3284,11 +3329,18 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3284
3329
  return res.end(JSON.stringify({ error: "password is required" }));
3285
3330
  }
3286
3331
 
3332
+ if (authHardLocked) {
3333
+ res.writeHead(401, { "Content-Type": "application/json", ...CORS });
3334
+ return res.end(JSON.stringify({ error: "Too many failed attempts — restart daemon to try again", hard_locked: true }));
3335
+ }
3336
+
3287
3337
  try {
3288
3338
  const { token, timestamp } = deriveToken(pw, machineHash);
3289
3339
  const result = await api.test(pw, machineHash, token, timestamp);
3290
3340
  if (result.error) throw new Error(result.error);
3291
3341
  password = pw; // unlock — store in process memory only
3342
+ authFailCount = 0;
3343
+ authHardLocked = false;
3292
3344
  const logLine = `[${new Date().toISOString()}] Vault unlocked\n`;
3293
3345
  try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
3294
3346
  // Start rotation engine on unlock
@@ -3340,9 +3392,19 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3340
3392
  }
3341
3393
  return ok(res, { ok: true, locked: false });
3342
3394
  } catch {
3343
- // Wrong password — not a lockout strike, just a UI auth attempt
3395
+ authFailCount++;
3396
+ const authRemaining = MAX_AUTH_FAILS - authFailCount;
3397
+ const failLog = `[${new Date().toISOString()}] [AUTH FAIL ${authFailCount}/${MAX_AUTH_FAILS}] Wrong password\n`;
3398
+ try { fs.appendFileSync(LOG_FILE, failLog); } catch {}
3399
+ if (authFailCount >= MAX_AUTH_FAILS) {
3400
+ authHardLocked = true;
3401
+ const lockLog = `[${new Date().toISOString()}] Auth failure limit reached — vault hard-locked\n`;
3402
+ try { fs.appendFileSync(LOG_FILE, lockLog); } catch {}
3403
+ res.writeHead(401, { "Content-Type": "application/json", ...CORS });
3404
+ return res.end(JSON.stringify({ error: "Too many failed attempts — restart daemon to try again", hard_locked: true }));
3405
+ }
3344
3406
  res.writeHead(401, { "Content-Type": "application/json", ...CORS });
3345
- return res.end(JSON.stringify({ error: "Invalid password" }));
3407
+ return res.end(JSON.stringify({ error: "Invalid password", failures_remaining: authRemaining }));
3346
3408
  }
3347
3409
  }
3348
3410
 
@@ -3352,7 +3414,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3352
3414
  stopTunnel();
3353
3415
  const logLine = `[${new Date().toISOString()}] Vault locked\n`;
3354
3416
  try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
3355
- return ok(res, { ok: true, locked: true });
3417
+ return ok(res, { ok: true, locked: true, hard_locked: authHardLocked });
3356
3418
  }
3357
3419
 
3358
3420
  // POST /toggle/:service — enable or disable a service
@@ -4046,7 +4108,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
4046
4108
  // Don't count browser noise, MCP discovery probes, or OAuth probes as auth failures
4047
4109
  const isBenign = reqPath.startsWith("/.well-known/") || [
4048
4110
  "/favicon.ico", "/robots.txt", "/apple-touch-icon.png", "/apple-touch-icon-precomposed.png",
4049
- "/sse", "/mcp", "/message", "/register", "/authorize", "/token",
4111
+ "/sse", "/mcp", "/message", "/register", "/authorize", "/token", "/shutdown",
4050
4112
  ].includes(reqPath);
4051
4113
  if (isBenign) {
4052
4114
  res.writeHead(404, { "Content-Type": "application/json", ...CORS });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {