@lifeaitools/clauth 1.4.3 → 1.4.5
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 +183 -17
- package/package.json +1 -1
package/cli/commands/serve.js
CHANGED
|
@@ -502,10 +502,18 @@ function dashboardHtml(port, whitelist, isStaged = false) {
|
|
|
502
502
|
.card:hover{border-color:#3b82f6}
|
|
503
503
|
.card-name{font-size:1rem;font-weight:600;color:#f8fafc;margin-bottom:3px}
|
|
504
504
|
.card-type{font-size:.78rem;color:#64748b;text-transform:uppercase;letter-spacing:.5px}
|
|
505
|
-
.card-getkey{font-size:.75rem;color:#
|
|
506
|
-
.card-getkey:hover{
|
|
505
|
+
.card-getkey{font-size:.75rem;color:#60a5fa;text-decoration:none;transition:color .15s}
|
|
506
|
+
.card-getkey:hover{color:#93c5fd;text-decoration:underline}
|
|
507
507
|
.card-value{font-family:'Courier New',monospace;font-size:.82rem;color:#22c55e;background:#0f172a;border-radius:4px;padding:8px 10px;margin-top:10px;word-break:break-all;max-height:80px;overflow:auto;display:none}
|
|
508
508
|
.card-actions{margin-top:10px;display:flex;gap:7px;flex-wrap:wrap}
|
|
509
|
+
.btn-rename{background:#1e293b;color:#94a3b8;border:1px solid #334155;padding:3px 8px;font-size:.75rem;border-radius:4px;cursor:pointer;transition:color .15s,border-color .15s}
|
|
510
|
+
.btn-rename:hover{color:#e2e8f0;border-color:#60a5fa}
|
|
511
|
+
.btn-delete{background:#1e293b;color:#f87171;border:1px solid #4b2020;padding:3px 8px;font-size:.75rem;border-radius:4px;cursor:pointer;transition:background .15s,border-color .15s}
|
|
512
|
+
.btn-delete:hover{background:#2d1f1f;border-color:#ef4444}
|
|
513
|
+
.rename-panel{display:none;margin-top:8px;background:#0f172a;border-radius:6px;padding:8px 10px;border:1px solid #1e3a5f}
|
|
514
|
+
.rename-input{width:calc(100% - 100px);background:#0a0f1a;border:1px solid #1e3a5f;border-radius:4px;color:#e2e8f0;font-size:.85rem;padding:5px 8px;outline:none}
|
|
515
|
+
.rename-input:focus{border-color:#3b82f6}
|
|
516
|
+
.rename-msg{font-size:.75rem;margin-left:6px}
|
|
509
517
|
.set-panel{display:none;margin-top:10px;background:#0f172a;border-radius:6px;padding:10px;border:1px solid #1e3a5f}
|
|
510
518
|
.set-panel label{font-size:.75rem;color:#64748b;display:block;margin-bottom:6px}
|
|
511
519
|
.set-input{width:100%;background:#0a0f1a;border:1px solid #1e3a5f;border-radius:4px;color:#e2e8f0;font-family:'Courier New',monospace;font-size:.85rem;padding:7px 10px;outline:none;resize:vertical;min-height:58px;transition:border-color .2s}
|
|
@@ -887,10 +895,12 @@ const KEY_URLS = {
|
|
|
887
895
|
// Extra links shown below the primary KEY_URLS link
|
|
888
896
|
const EXTRA_LINKS = {
|
|
889
897
|
"gmail": [
|
|
898
|
+
{ label: "↗ Open Gmail", url: "https://mail.google.com" },
|
|
890
899
|
{ label: "↗ OAuth Playground (get refresh token)", url: "https://developers.google.com/oauthplayground/" },
|
|
891
900
|
{ label: "↗ Enable Gmail API", url: "https://console.cloud.google.com/apis/library/gmail.googleapis.com" },
|
|
892
901
|
],
|
|
893
902
|
"gcal": [
|
|
903
|
+
{ label: "↗ Open Google Calendar", url: "https://calendar.google.com" },
|
|
894
904
|
{ label: "↗ OAuth Playground (get refresh token)", url: "https://developers.google.com/oauthplayground/" },
|
|
895
905
|
{ label: "↗ Enable Calendar API", url: "https://console.cloud.google.com/apis/library/calendar-json.googleapis.com" },
|
|
896
906
|
],
|
|
@@ -959,20 +969,35 @@ async function boot() {
|
|
|
959
969
|
try {
|
|
960
970
|
const ping = await fetch(BASE + "/ping").then(r => r.json());
|
|
961
971
|
if (ping.locked) {
|
|
962
|
-
showLockScreen();
|
|
972
|
+
showLockScreen(ping.hard_locked);
|
|
963
973
|
} else {
|
|
964
974
|
showMain(ping);
|
|
965
975
|
loadServices();
|
|
966
976
|
}
|
|
967
977
|
} catch {
|
|
968
|
-
showLockScreen();
|
|
978
|
+
showLockScreen(false);
|
|
969
979
|
}
|
|
970
980
|
}
|
|
971
981
|
|
|
972
|
-
function showLockScreen() {
|
|
982
|
+
function showLockScreen(hardLocked) {
|
|
973
983
|
document.getElementById("lock-screen").style.display = "flex";
|
|
974
984
|
document.getElementById("main-view").style.display = "none";
|
|
975
|
-
|
|
985
|
+
const input = document.getElementById("lock-input");
|
|
986
|
+
const btn = document.getElementById("unlock-btn");
|
|
987
|
+
const sub = document.getElementById("lock-sub");
|
|
988
|
+
const err = document.getElementById("lock-err");
|
|
989
|
+
if (hardLocked) {
|
|
990
|
+
input.disabled = true;
|
|
991
|
+
btn.disabled = true;
|
|
992
|
+
sub.textContent = "Too many failed attempts";
|
|
993
|
+
err.textContent = "✗ Vault locked — restart daemon to try again";
|
|
994
|
+
} else {
|
|
995
|
+
input.disabled = false;
|
|
996
|
+
btn.disabled = false;
|
|
997
|
+
sub.textContent = "Paste your password to unlock";
|
|
998
|
+
err.textContent = "";
|
|
999
|
+
setTimeout(() => input.focus(), 50);
|
|
1000
|
+
}
|
|
976
1001
|
}
|
|
977
1002
|
|
|
978
1003
|
// ── Upgrade detection ────────────────────────────────────────────
|
|
@@ -1011,6 +1036,11 @@ function showMain(ping) {
|
|
|
1011
1036
|
checkUpgrade(ping);
|
|
1012
1037
|
document.getElementById("lock-screen").style.display = "none";
|
|
1013
1038
|
document.getElementById("main-view").style.display = "block";
|
|
1039
|
+
// Reset lock screen state for next lock
|
|
1040
|
+
document.getElementById("lock-input").disabled = false;
|
|
1041
|
+
document.getElementById("unlock-btn").disabled = false;
|
|
1042
|
+
document.getElementById("lock-sub").textContent = "Paste your password to unlock";
|
|
1043
|
+
document.getElementById("lock-err").textContent = "";
|
|
1014
1044
|
if (ping) {
|
|
1015
1045
|
document.getElementById("s-pid").textContent = ping.pid || "—";
|
|
1016
1046
|
document.getElementById("s-fails").textContent =
|
|
@@ -1025,8 +1055,10 @@ async function unlock() {
|
|
|
1025
1055
|
const input = document.getElementById("lock-input");
|
|
1026
1056
|
const btn = document.getElementById("unlock-btn");
|
|
1027
1057
|
const err = document.getElementById("lock-err");
|
|
1058
|
+
const sub = document.getElementById("lock-sub");
|
|
1028
1059
|
const pw = input.value;
|
|
1029
1060
|
|
|
1061
|
+
if (input.disabled) return;
|
|
1030
1062
|
if (!pw) { err.textContent = "Password is required."; return; }
|
|
1031
1063
|
|
|
1032
1064
|
btn.disabled = true; btn.textContent = "Verifying…"; err.textContent = "";
|
|
@@ -1038,7 +1070,22 @@ async function unlock() {
|
|
|
1038
1070
|
body: JSON.stringify({ password: pw })
|
|
1039
1071
|
}).then(r => r.json());
|
|
1040
1072
|
|
|
1041
|
-
if (r.
|
|
1073
|
+
if (r.hard_locked) {
|
|
1074
|
+
input.value = "";
|
|
1075
|
+
input.disabled = true;
|
|
1076
|
+
btn.disabled = true;
|
|
1077
|
+
btn.textContent = "Unlock";
|
|
1078
|
+
sub.textContent = "Too many failed attempts";
|
|
1079
|
+
err.textContent = "✗ Vault locked — restart daemon to try again";
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (r.error) {
|
|
1084
|
+
const rem = r.failures_remaining !== undefined
|
|
1085
|
+
? \` (\${r.failures_remaining} attempt\${r.failures_remaining !== 1 ? "s" : ""} remaining)\`
|
|
1086
|
+
: "";
|
|
1087
|
+
throw new Error(r.error + rem);
|
|
1088
|
+
}
|
|
1042
1089
|
|
|
1043
1090
|
input.value = "";
|
|
1044
1091
|
const ping = await fetch(BASE + "/ping").then(r => r.json());
|
|
@@ -1050,14 +1097,14 @@ async function unlock() {
|
|
|
1050
1097
|
err.textContent = "✗ " + (e.message || "Invalid password");
|
|
1051
1098
|
setTimeout(() => input.className = "lock-input", 600);
|
|
1052
1099
|
} finally {
|
|
1053
|
-
btn.disabled = false; btn.textContent = "Unlock";
|
|
1100
|
+
if (!input.disabled) { btn.disabled = false; btn.textContent = "Unlock"; }
|
|
1054
1101
|
}
|
|
1055
1102
|
}
|
|
1056
1103
|
|
|
1057
1104
|
// ── Lock ────────────────────────────────────
|
|
1058
1105
|
async function lockVault() {
|
|
1059
|
-
await fetch(BASE + "/lock", { method: "POST" }).catch(() => {});
|
|
1060
|
-
showLockScreen();
|
|
1106
|
+
const r = await fetch(BASE + "/lock", { method: "POST" }).then(r => r.json()).catch(() => ({}));
|
|
1107
|
+
showLockScreen(r.hard_locked || false);
|
|
1061
1108
|
}
|
|
1062
1109
|
|
|
1063
1110
|
// ── Make Live (blue-green: promote staged instance to live port) ──
|
|
@@ -1155,6 +1202,14 @@ function renderServiceGrid(services) {
|
|
|
1155
1202
|
<button class="btn-project" onclick="toggleProjectEdit('\${s.name}')">\${s.project ? "✎ Project" : "+ Project"}</button>
|
|
1156
1203
|
<button class="btn \${s.enabled === false ? "btn-enable" : "btn-disable"}" id="togbtn-\${s.name}" onclick="toggleService('\${s.name}')">\${s.enabled === false ? "Enable" : "Disable"}</button>
|
|
1157
1204
|
<button class="btn-rotate" id="rotbtn-\${s.name}" style="display:none;background:#0e7490;border:1px solid #06b6d4;color:#cffafe;font-size:.75rem;padding:3px 8px;border-radius:4px;cursor:pointer" onclick="rotateKey('\${s.name}')">↻ Rotate</button>
|
|
1205
|
+
<button class="btn-rename" onclick="toggleRename('\${s.name}')" title="Rename service">✎</button>
|
|
1206
|
+
<button class="btn-delete" onclick="deleteService('\${s.name}')" title="Delete service">✕</button>
|
|
1207
|
+
</div>
|
|
1208
|
+
<div class="rename-panel" id="rn-\${s.name}">
|
|
1209
|
+
<input class="rename-input" id="rn-input-\${s.name}" value="\${s.name}" spellcheck="false" autocomplete="off" placeholder="New name…">
|
|
1210
|
+
<button class="btn" onclick="saveRename('\${s.name}')" style="padding:4px 10px;font-size:.8rem">Save</button>
|
|
1211
|
+
<button class="btn" onclick="toggleRename('\${s.name}')" style="padding:4px 10px;font-size:.8rem;background:#1e293b">Cancel</button>
|
|
1212
|
+
<span class="rename-msg" id="rn-msg-\${s.name}"></span>
|
|
1158
1213
|
</div>
|
|
1159
1214
|
<div class="project-edit" id="pe-\${s.name}">
|
|
1160
1215
|
<input type="text" id="pe-input-\${s.name}" value="\${s.project || ""}" placeholder="Project name…" spellcheck="false" autocomplete="off">
|
|
@@ -1360,6 +1415,53 @@ async function clearProject(name) {
|
|
|
1360
1415
|
await saveProject(name);
|
|
1361
1416
|
}
|
|
1362
1417
|
|
|
1418
|
+
// ── Rename service ───────────────────────────
|
|
1419
|
+
function toggleRename(name) {
|
|
1420
|
+
const panel = document.getElementById("rn-" + name);
|
|
1421
|
+
const open = panel.style.display === "block";
|
|
1422
|
+
panel.style.display = open ? "none" : "block";
|
|
1423
|
+
if (!open) {
|
|
1424
|
+
const inp = document.getElementById("rn-input-" + name);
|
|
1425
|
+
inp.value = name;
|
|
1426
|
+
inp.focus(); inp.select();
|
|
1427
|
+
document.getElementById("rn-msg-" + name).textContent = "";
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
async function saveRename(oldName) {
|
|
1432
|
+
const inp = document.getElementById("rn-input-" + oldName);
|
|
1433
|
+
const msg = document.getElementById("rn-msg-" + oldName);
|
|
1434
|
+
const newName = inp.value.trim();
|
|
1435
|
+
if (!newName || newName === oldName) { toggleRename(oldName); return; }
|
|
1436
|
+
msg.style.color = "#94a3b8"; msg.textContent = "Saving…";
|
|
1437
|
+
try {
|
|
1438
|
+
const r = await fetch(BASE + "/rename/" + oldName, {
|
|
1439
|
+
method: "POST",
|
|
1440
|
+
headers: { "Content-Type": "application/json" },
|
|
1441
|
+
body: JSON.stringify({ new_name: newName })
|
|
1442
|
+
}).then(r => r.json());
|
|
1443
|
+
if (r.locked) { showLockScreen(false); return; }
|
|
1444
|
+
if (r.error) throw new Error(r.error);
|
|
1445
|
+
msg.style.color = "#4ade80"; msg.textContent = "✓ Renamed";
|
|
1446
|
+
setTimeout(() => loadServices(), 800);
|
|
1447
|
+
} catch (e) {
|
|
1448
|
+
msg.style.color = "#f87171"; msg.textContent = "✗ " + e.message;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// ── Delete service ───────────────────────────
|
|
1453
|
+
async function deleteService(name) {
|
|
1454
|
+
if (!confirm(\`Delete service "\${name}"? This cannot be undone.\`)) return;
|
|
1455
|
+
try {
|
|
1456
|
+
const r = await fetch(BASE + "/delete/" + name, { method: "POST" }).then(r => r.json());
|
|
1457
|
+
if (r.locked) { showLockScreen(false); return; }
|
|
1458
|
+
if (r.error) throw new Error(r.error);
|
|
1459
|
+
loadServices();
|
|
1460
|
+
} catch (e) {
|
|
1461
|
+
alert("Delete failed: " + e.message);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1363
1465
|
// ── Set key ─────────────────────────────────
|
|
1364
1466
|
function toggleSet(name) {
|
|
1365
1467
|
const panel = document.getElementById("set-panel-" + name);
|
|
@@ -2279,6 +2381,9 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2279
2381
|
}
|
|
2280
2382
|
const MAX_FAILS = 10;
|
|
2281
2383
|
let failCount = 0;
|
|
2384
|
+
const MAX_AUTH_FAILS = 10;
|
|
2385
|
+
let authFailCount = 0;
|
|
2386
|
+
let authHardLocked = false;
|
|
2282
2387
|
let password = initPassword || null; // null = locked; set via POST /auth
|
|
2283
2388
|
const machineHash = getMachineHash();
|
|
2284
2389
|
|
|
@@ -2487,17 +2592,18 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2487
2592
|
error: message,
|
|
2488
2593
|
failures: failCount,
|
|
2489
2594
|
failures_remaining: remaining,
|
|
2490
|
-
...(failCount >= MAX_FAILS ? {
|
|
2595
|
+
...(failCount >= MAX_FAILS ? { locked: true } : {})
|
|
2491
2596
|
});
|
|
2492
2597
|
|
|
2493
2598
|
res.writeHead(code, { "Content-Type": "application/json", ...CORS });
|
|
2494
2599
|
res.end(body);
|
|
2495
2600
|
|
|
2496
2601
|
if (failCount >= MAX_FAILS) {
|
|
2497
|
-
const msg = `[${new Date().toISOString()}] Failure limit reached —
|
|
2602
|
+
const msg = `[${new Date().toISOString()}] Failure limit reached — locking vault\n`;
|
|
2498
2603
|
try { fs.appendFileSync(LOG_FILE, msg); } catch {}
|
|
2499
|
-
|
|
2500
|
-
|
|
2604
|
+
password = null;
|
|
2605
|
+
failCount = 0;
|
|
2606
|
+
stopTunnel();
|
|
2501
2607
|
}
|
|
2502
2608
|
}
|
|
2503
2609
|
|
|
@@ -3013,8 +3119,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3013
3119
|
status: "ok",
|
|
3014
3120
|
pid: process.pid,
|
|
3015
3121
|
locked: !password,
|
|
3122
|
+
hard_locked: authHardLocked,
|
|
3016
3123
|
failures: failCount,
|
|
3017
3124
|
failures_remaining: MAX_FAILS - failCount,
|
|
3125
|
+
auth_failures: authFailCount,
|
|
3126
|
+
auth_failures_remaining: MAX_AUTH_FAILS - authFailCount,
|
|
3018
3127
|
services: whitelist || "all",
|
|
3019
3128
|
port,
|
|
3020
3129
|
tunnel_status: tunnelStatus,
|
|
@@ -3285,11 +3394,18 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3285
3394
|
return res.end(JSON.stringify({ error: "password is required" }));
|
|
3286
3395
|
}
|
|
3287
3396
|
|
|
3397
|
+
if (authHardLocked) {
|
|
3398
|
+
res.writeHead(401, { "Content-Type": "application/json", ...CORS });
|
|
3399
|
+
return res.end(JSON.stringify({ error: "Too many failed attempts — restart daemon to try again", hard_locked: true }));
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3288
3402
|
try {
|
|
3289
3403
|
const { token, timestamp } = deriveToken(pw, machineHash);
|
|
3290
3404
|
const result = await api.test(pw, machineHash, token, timestamp);
|
|
3291
3405
|
if (result.error) throw new Error(result.error);
|
|
3292
3406
|
password = pw; // unlock — store in process memory only
|
|
3407
|
+
authFailCount = 0;
|
|
3408
|
+
authHardLocked = false;
|
|
3293
3409
|
const logLine = `[${new Date().toISOString()}] Vault unlocked\n`;
|
|
3294
3410
|
try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
|
|
3295
3411
|
// Start rotation engine on unlock
|
|
@@ -3341,9 +3457,19 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3341
3457
|
}
|
|
3342
3458
|
return ok(res, { ok: true, locked: false });
|
|
3343
3459
|
} catch {
|
|
3344
|
-
|
|
3460
|
+
authFailCount++;
|
|
3461
|
+
const authRemaining = MAX_AUTH_FAILS - authFailCount;
|
|
3462
|
+
const failLog = `[${new Date().toISOString()}] [AUTH FAIL ${authFailCount}/${MAX_AUTH_FAILS}] Wrong password\n`;
|
|
3463
|
+
try { fs.appendFileSync(LOG_FILE, failLog); } catch {}
|
|
3464
|
+
if (authFailCount >= MAX_AUTH_FAILS) {
|
|
3465
|
+
authHardLocked = true;
|
|
3466
|
+
const lockLog = `[${new Date().toISOString()}] Auth failure limit reached — vault hard-locked\n`;
|
|
3467
|
+
try { fs.appendFileSync(LOG_FILE, lockLog); } catch {}
|
|
3468
|
+
res.writeHead(401, { "Content-Type": "application/json", ...CORS });
|
|
3469
|
+
return res.end(JSON.stringify({ error: "Too many failed attempts — restart daemon to try again", hard_locked: true }));
|
|
3470
|
+
}
|
|
3345
3471
|
res.writeHead(401, { "Content-Type": "application/json", ...CORS });
|
|
3346
|
-
return res.end(JSON.stringify({ error: "Invalid password" }));
|
|
3472
|
+
return res.end(JSON.stringify({ error: "Invalid password", failures_remaining: authRemaining }));
|
|
3347
3473
|
}
|
|
3348
3474
|
}
|
|
3349
3475
|
|
|
@@ -3353,7 +3479,47 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3353
3479
|
stopTunnel();
|
|
3354
3480
|
const logLine = `[${new Date().toISOString()}] Vault locked\n`;
|
|
3355
3481
|
try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
|
|
3356
|
-
return ok(res, { ok: true, locked: true });
|
|
3482
|
+
return ok(res, { ok: true, locked: true, hard_locked: authHardLocked });
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
// POST /rename/:service — rename a service
|
|
3486
|
+
const renameMatch = reqPath.match(/^\/rename\/([a-zA-Z0-9_-]+)$/);
|
|
3487
|
+
if (method === "POST" && renameMatch) {
|
|
3488
|
+
if (lockedGuard(res)) return;
|
|
3489
|
+
const service = renameMatch[1].toLowerCase();
|
|
3490
|
+
let body;
|
|
3491
|
+
try { body = await readBody(req); } catch {
|
|
3492
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3493
|
+
return res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
3494
|
+
}
|
|
3495
|
+
const newName = (body.new_name || "").trim().toLowerCase();
|
|
3496
|
+
if (!newName || !/^[a-z0-9_-]+$/.test(newName)) {
|
|
3497
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3498
|
+
return res.end(JSON.stringify({ error: "Invalid name — use lowercase letters, numbers, hyphens, underscores" }));
|
|
3499
|
+
}
|
|
3500
|
+
try {
|
|
3501
|
+
const { token, timestamp } = deriveToken(password, machineHash);
|
|
3502
|
+
const result = await api.updateService(password, machineHash, token, timestamp, service, { name: newName, label: newName });
|
|
3503
|
+
if (result.error) return strike(res, 502, result.error);
|
|
3504
|
+
return ok(res, { ok: true, old_name: service, new_name: newName });
|
|
3505
|
+
} catch (err) {
|
|
3506
|
+
return strike(res, 502, err.message);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
// POST /delete/:service — remove a service entirely
|
|
3511
|
+
const deleteMatch = reqPath.match(/^\/delete\/([a-zA-Z0-9_-]+)$/);
|
|
3512
|
+
if (method === "POST" && deleteMatch) {
|
|
3513
|
+
if (lockedGuard(res)) return;
|
|
3514
|
+
const service = deleteMatch[1].toLowerCase();
|
|
3515
|
+
try {
|
|
3516
|
+
const { token, timestamp } = deriveToken(password, machineHash);
|
|
3517
|
+
const result = await api.removeService(password, machineHash, token, timestamp, service, true);
|
|
3518
|
+
if (result.error) return strike(res, 502, result.error);
|
|
3519
|
+
return ok(res, { ok: true, deleted: service });
|
|
3520
|
+
} catch (err) {
|
|
3521
|
+
return strike(res, 502, err.message);
|
|
3522
|
+
}
|
|
3357
3523
|
}
|
|
3358
3524
|
|
|
3359
3525
|
// POST /toggle/:service — enable or disable a service
|