fix(housekeeping): correct people partial audit evidence

This commit is contained in:
Simo committed 2026-08-29 15:24:19 +02:00
1 parent 91c9efcbb4
commit fcd1dfd96e
5 files changed
+339 -89

No files matched your search

@@ -118,6 +118,8 @@ class PeopleMutationFailure extends Error {
}
}
class ConfirmedExternalNoopFailure extends PeopleMutationFailure {}
function operationCapability(operation: PeopleMutationOperation) {
if (operation === "user.ban" || operation === "user.unban") {
return anyCapability(PERMS.USERS_BAN);
@@ -307,7 +309,7 @@ function userIds(value: unknown, legacy: boolean): number[] {
async function requireRcon(result: boolean): Promise<void> {
if (!result) {
throw new PeopleMutationFailure(
throw new ConfirmedExternalNoopFailure(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
);
@@ -402,10 +404,16 @@ async function finalizeExternalWithAudit(
snapshot: PeopleMutationSnapshot,
execute: () => Promise<void>,
mutationCommitted = true,
confirmedFailureSnapshot?: PeopleMutationSnapshot,
): Promise<PeopleMutationSnapshot> {
try {
await execute();
} catch (error) {
const failureSnapshot = mutationCommitted
? snapshot
: error instanceof ConfirmedExternalNoopFailure
? (confirmedFailureSnapshot ?? { before: snapshot.before, after: null })
: { before: snapshot.before, after: null };
let audit: HousekeepingPartialCompletion["audit"] = "persisted";
try {
await logAudit(
@@ -414,7 +422,7 @@ async function finalizeExternalWithAudit(
operation,
target,
targetId,
snapshot,
failureSnapshot,
mutationCommitted ? "partial" : "failure",
),
);
@@ -482,6 +490,7 @@ async function runExternalWithAudit(
targetId: number | undefined,
snapshot: PeopleMutationSnapshot,
execute: () => Promise<void>,
confirmedFailureSnapshot: PeopleMutationSnapshot,
): Promise<PeopleMutationSnapshot> {
await logAudit(
canonicalAuditEntry(
@@ -501,6 +510,7 @@ async function runExternalWithAudit(
snapshot,
execute,
false,
confirmedFailureSnapshot,
);
}
async function executeUserMutation(
@@ -524,6 +534,7 @@ async function executeUserMutation(
userId,
snapshot,
async () => requireRcon(await rcon.alertUser(userId, message)),
{ before: null, after: { userId, alertDelivered: false } },
);
}
@@ -592,13 +603,12 @@ async function executeUserMutation(
snapshot,
async () => {
invalidateLoginCache(target.username);
const notification = notify({
void notify({
action: "user_edit",
actor: context.capability.actor.username,
target: target.username,
targetId: userId,
});
if (!context.legacy) await notification;
},
);
}
@@ -704,7 +714,7 @@ async function executeUserMutation(
snapshot,
async () => {
await requireRcon(await rcon.disconnectUser(userId));
await notify({
void notify({
action: "ban",
actor: context.capability.actor.username,
target: target.username,
@@ -787,7 +797,7 @@ async function executeUserMutation(
);
return value;
});
notify({
void notify({
action: "unban",
actor: context.capability.actor.username,
target: target.username,
@@ -805,12 +815,13 @@ async function executeUserMutation(
snapshot,
async () => {
await requireRcon(await rcon.disconnectUser(userId));
await notify({
void notify({
action: "disconnect",
actor: context.capability.actor.username,
target: target.username,
});
},
{ before, after: before },
);
}
@@ -838,6 +849,7 @@ async function executeUserMutation(
await requireRcon(await rcon.unmuteUser(userId));
}
},
{ before, after: before },
);
}
@@ -877,13 +889,12 @@ async function executeUserMutation(
snapshot,
async () => {
invalidateLoginCache(target.username);
const notification = notify({
void notify({
action: "user_edit",
actor: context.capability.actor.username,
target: target.username,
details: "Password reset",
});
if (!context.legacy) await notification;
},
);
}
@@ -898,6 +909,7 @@ async function executeUserMutation(
userId,
snapshot,
async () => requireRcon(await rcon.giveCredits(userId, amount)),
{ before, after: before },
);
}
@@ -1014,8 +1026,10 @@ async function executeBulkMutation(
const data = record(input);
const ids = userIds(data.userIds, context.legacy);
const failures: Array<{ userId: number; reason: string }> = [];
const externalSyncFailures: Array<{ userId: number; reason: string }> = [];
const externalSyncFailureReason =
"Database applied; emulator sync failed. Do not retry automatically.";
let completed = 0;
let externalFailed = false;
let mutationReason: string | undefined;
if (operation === "users.bulk-unban") {
@@ -1126,16 +1140,12 @@ async function executeBulkMutation(
| "pixels"
| "points";
for (const userId of ids) {
let databaseCommitted = false;
try {
if (type === "credits") {
await db
.update(User)
.set({ credits: sql`${User.credits} + ${amount}` })
.where(eq(User.id, userId));
databaseCommitted = true;
const delivered = await rcon.giveCredits(userId, amount);
if (!context.legacy) await requireRcon(delivered);
} else {
const currencyType = type === "pixels" ? 0 : 101;
await db
@@ -1144,19 +1154,25 @@ async function executeBulkMutation(
.onDuplicateKeyUpdate({
set: { amount: sql`${UsersCurrency.amount} + ${amount}` },
});
databaseCommitted = true;
if (type === "pixels") {
const delivered = await rcon.giveDuckets(userId, amount);
if (!context.legacy) await requireRcon(delivered);
} else {
const delivered = await rcon.givePointsGotw(userId, amount);
if (!context.legacy) await requireRcon(delivered);
}
}
completed += 1;
} catch {
if (databaseCommitted) externalFailed = true;
failures.push({ userId, reason: "Database error" });
continue;
}
completed += 1;
try {
const delivered =
type === "credits"
? await rcon.giveCredits(userId, amount)
: type === "pixels"
? await rcon.giveDuckets(userId, amount)
: await rcon.givePointsGotw(userId, amount);
if (!context.legacy) await requireRcon(delivered);
} catch {
externalSyncFailures.push({
userId,
reason: externalSyncFailureReason,
});
}
}
await logStaffActivity({
@@ -1168,7 +1184,7 @@ async function executeBulkMutation(
} else {
const badgeCode = normalizedText(data.badgeCode, 20);
for (const userId of ids) {
let databaseCommitted = false;
let shouldSync = false;
try {
const [existing] = await db
.select({ id: UsersBadges.id })
@@ -1190,14 +1206,22 @@ async function executeBulkMutation(
slotId: (aggregate?.maxSlot ?? 0) + 1,
badgeCode,
});
databaseCommitted = true;
const delivered = await rcon.giveBadge(userId, badgeCode);
if (!context.legacy) await requireRcon(delivered);
shouldSync = true;
}
completed += 1;
} catch {
if (databaseCommitted) externalFailed = true;
failures.push({ userId, reason: "Database error" });
continue;
}
if (!shouldSync) continue;
try {
const delivered = await rcon.giveBadge(userId, badgeCode);
if (!context.legacy) await requireRcon(delivered);
} catch {
externalSyncFailures.push({
userId,
reason: externalSyncFailureReason,
});
}
}
await logStaffActivity({
@@ -1208,11 +1232,13 @@ async function executeBulkMutation(
});
}
const hasPartialCompletion =
failures.length > 0 || externalSyncFailures.length > 0;
const completion: HousekeepingPartialCompletion | undefined =
failures.length > 0
hasPartialCompletion
? {
status: "partial",
external: externalFailed ? "failed" : "not-required",
external: externalSyncFailures.length > 0 ? "failed" : "not-required",
audit: "persisted",
}
: undefined;
@@ -1222,6 +1248,7 @@ async function executeBulkMutation(
completed,
total: ids.length,
failedIds: failures,
...(externalSyncFailures.length > 0 ? { externalSyncFailures } : {}),
...(mutationReason ? { reason: mutationReason } : {}),
},
...(completion === undefined ? {} : { completion }),
@@ -1234,7 +1261,7 @@ async function executeBulkMutation(
"User",
undefined,
snapshot,
failures.length > 0 ? "partial" : "success",
hasPartialCompletion ? "partial" : "success",
),
);
} catch (auditOutcomeError) {