Whitelist: write allowlist.json+whitelist.json, verify after reload, learn UUIDs from logs

On 26.2 the file whitelist entries load at startup but 'whitelist reload'
and shutdown saves use the renamed allowlist.json, so panel writes never
took effect live and got wiped on stop. Now the panel writes both file
names, verifies the player actually appears in 'whitelist list' after
reload, falls back to the vanilla add command, and captures each player's
real UUID from 'UUID of player …' log lines for future entries. The auth
watcher's self-heal loop reports a loud notification after 3 failed
retries instead of silently spamming reloads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Aryan SinghandClaude Fable 5 committed 2026-07-11 17:16:22 -05:00
1 parent b2ff88b255
commit b043f03d28
3 files changed
+96 -16

No files matched your search

+32 -2
View File
@@ -157,6 +157,27 @@ export async function importFromWhitelist(): Promise<number> {
const g = globalThis as unknown as { __craftdeckAuthWatch?: { timer: NodeJS.Timeout } };
/**
* Consecutive whitelist-heal failures per player. After a few strikes the
* admin gets ONE loud notification instead of a silent 5-second retry loop.
*/
const healFailures = new Map<string, { count: number; notified: boolean }>();
function reportHealFailure(username: string, message: string) {
const entry = healFailures.get(username) ?? { count: 0, notified: false };
entry.count++;
if (entry.count >= 3 && !entry.notified) {
entry.notified = true;
notify(
"error",
`Whitelist is not applying for ${username}`,
`The server keeps rejecting the entry after several retries (${message}). ` +
`Check the Console page and docs/troubleshooting.md — the panel will keep retrying meanwhile.`
);
}
healFailures.set(username, entry);
}
async function reconcile() {
const pending = db()
.prepare("SELECT * FROM approved_players WHERE status = 'pending'")
@@ -183,14 +204,23 @@ async function reconcile() {
const live = new Set((await whitelistLive()).map((n) => n.toLowerCase()));
const missing = pending.filter((p) => !live.has(p.username.toLowerCase()));
if (missing.length > 0) {
for (const p of missing) await whitelistAdd(p.username); // rewrites file + reloads
let healed = true;
for (const p of missing) {
try {
await whitelistAdd(p.username); // rewrites files + reloads + verifies
} catch (err) {
healed = false;
reportHealFailure(p.username, (err as Error).message);
}
}
// They couldn't have joined yet — give the windows back the lost time.
db()
.prepare("UPDATE approved_players SET window_expires_at = window_expires_at + 5000 WHERE status = 'pending'")
.run();
logActivity("whitelist_heal", null, missing.map((m) => m.username).join(", "));
if (healed) logActivity("whitelist_heal", null, missing.map((m) => m.username).join(", "));
return;
}
healFailures.clear(); // whitelist is consistent again
} catch {
// whitelist list unavailable — fall through and try again next tick
}