@lifeaitools/clauth 1.4.3 → 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.
- package/cli/commands/serve.js +76 -15
- package/package.json +1 -1
package/cli/commands/serve.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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 ? {
|
|
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 —
|
|
2537
|
+
const msg = `[${new Date().toISOString()}] Failure limit reached — locking vault\n`;
|
|
2498
2538
|
try { fs.appendFileSync(LOG_FILE, msg); } catch {}
|
|
2499
|
-
|
|
2500
|
-
|
|
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,
|
|
@@ -3285,11 +3329,18 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3285
3329
|
return res.end(JSON.stringify({ error: "password is required" }));
|
|
3286
3330
|
}
|
|
3287
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
|
+
|
|
3288
3337
|
try {
|
|
3289
3338
|
const { token, timestamp } = deriveToken(pw, machineHash);
|
|
3290
3339
|
const result = await api.test(pw, machineHash, token, timestamp);
|
|
3291
3340
|
if (result.error) throw new Error(result.error);
|
|
3292
3341
|
password = pw; // unlock — store in process memory only
|
|
3342
|
+
authFailCount = 0;
|
|
3343
|
+
authHardLocked = false;
|
|
3293
3344
|
const logLine = `[${new Date().toISOString()}] Vault unlocked\n`;
|
|
3294
3345
|
try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
|
|
3295
3346
|
// Start rotation engine on unlock
|
|
@@ -3341,9 +3392,19 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3341
3392
|
}
|
|
3342
3393
|
return ok(res, { ok: true, locked: false });
|
|
3343
3394
|
} catch {
|
|
3344
|
-
|
|
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
|
+
}
|
|
3345
3406
|
res.writeHead(401, { "Content-Type": "application/json", ...CORS });
|
|
3346
|
-
return res.end(JSON.stringify({ error: "Invalid password" }));
|
|
3407
|
+
return res.end(JSON.stringify({ error: "Invalid password", failures_remaining: authRemaining }));
|
|
3347
3408
|
}
|
|
3348
3409
|
}
|
|
3349
3410
|
|
|
@@ -3353,7 +3414,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3353
3414
|
stopTunnel();
|
|
3354
3415
|
const logLine = `[${new Date().toISOString()}] Vault locked\n`;
|
|
3355
3416
|
try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
|
|
3356
|
-
return ok(res, { ok: true, locked: true });
|
|
3417
|
+
return ok(res, { ok: true, locked: true, hard_locked: authHardLocked });
|
|
3357
3418
|
}
|
|
3358
3419
|
|
|
3359
3420
|
// POST /toggle/:service — enable or disable a service
|