Revert "Merge pull request 'Complete Housekeeping migration and /ase cutover' (#52) from codex/housekeeping-complete into main"
CI / check (push) Successful in 27s
CI / release (push) Skipped
CI / deploy (push) Successful in 43s

This reverts commit 488b6e57c4, reversing
changes made to b506b4499a.
This commit is contained in:
Simo committed 2026-08-30 21:31:34 +02:00
1 parent 488b6e57c4
commit b1ddda66ff
802 files changed
+61370 -76659

No files matched your search

@@ -1,403 +0,0 @@
import "server-only";
import { eq } from "drizzle-orm";
import { Ban, type Db, SupportTickets, User } from "@/lib/db";
import type { AuditEntry, HousekeepingAuditWriter } from "@/lib/services/audit";
import type {
ModerationAction,
ModerationActionTransport,
} from "@/lib/services/moderation";
import type {
HousekeepingCapabilityContext,
HousekeepingErrorCode,
HousekeepingPartialCompletion,
} from "../../../foundation/contracts";
export const PEOPLE_MODERATION_MUTATION_OPERATIONS = [
"cfh.sanction",
"ban.create",
"ban.lift",
"moderation.action",
] as const;
export type PeopleModerationMutationOperation =
(typeof PEOPLE_MODERATION_MUTATION_OPERATIONS)[number];
export interface PeopleModerationMutationContext {
readonly capability: HousekeepingCapabilityContext;
readonly correlationId: string;
readonly legacy: boolean;
}
export interface PeopleModerationMutationSnapshot {
readonly before: Readonly<Record<string, unknown>> | null;
readonly after: Readonly<Record<string, unknown>> | null;
readonly output?: Readonly<Record<string, unknown>>;
readonly completion?: HousekeepingPartialCompletion;
}
type AuditOutcome = "intent" | "success" | "failure" | "partial";
type ModerationTransport = ModerationActionTransport & {
disconnectUser(userId: number, username?: string): Promise<boolean>;
};
export interface PeopleModerationMutationDependencies {
readonly db: Db;
readonly writeAudit: HousekeepingAuditWriter["write"];
readonly auditEntry: (
context: PeopleModerationMutationContext,
operation: PeopleModerationMutationOperation,
target: string,
targetId: number | undefined,
snapshot: PeopleModerationMutationSnapshot,
outcome: AuditOutcome,
) => AuditEntry;
readonly failure: (code: HousekeepingErrorCode, messageKey: string) => Error;
readonly record: (input: unknown) => Record<string, unknown>;
readonly positiveInteger: (value: unknown) => number;
readonly nonNegativeInteger: (value: unknown) => number;
readonly normalizedText: (
value: unknown,
max: number,
required?: boolean,
) => string;
readonly requireRcon: (result: boolean) => Promise<void>;
readonly transport: ModerationTransport;
readonly executeModerationAction: (
transport: ModerationActionTransport,
input: ModerationAction,
) => Promise<boolean>;
readonly logStaffActivity: (input: {
staffId: number;
action: string;
description: string;
targetType?: string;
targetId?: number;
}) => Promise<void>;
readonly runExternalWithAudit: (
context: PeopleModerationMutationContext,
operation: PeopleModerationMutationOperation,
target: string,
targetId: number | undefined,
snapshot: PeopleModerationMutationSnapshot,
execute: () => Promise<void>,
confirmedFailureSnapshot: PeopleModerationMutationSnapshot,
) => Promise<PeopleModerationMutationSnapshot>;
readonly finalizeExternalWithAudit: (
context: PeopleModerationMutationContext,
operation: PeopleModerationMutationOperation,
target: string,
targetId: number | undefined,
snapshot: PeopleModerationMutationSnapshot,
execute: () => Promise<void>,
mutationCommitted?: boolean,
confirmedFailureSnapshot?: PeopleModerationMutationSnapshot,
) => Promise<PeopleModerationMutationSnapshot>;
}
export interface PeopleModerationMutationExecutor {
execute(
operation: PeopleModerationMutationOperation,
input: unknown,
context: PeopleModerationMutationContext,
): Promise<PeopleModerationMutationSnapshot>;
}
export function isPeopleModerationMutationOperation(
operation: string,
): operation is PeopleModerationMutationOperation {
return (PEOPLE_MODERATION_MUTATION_OPERATIONS as readonly string[]).includes(
operation,
);
}
export function createPeopleModerationMutationExecutor(
dependencies: PeopleModerationMutationDependencies,
): PeopleModerationMutationExecutor {
const {
db,
writeAudit,
auditEntry,
failure,
record,
positiveInteger,
nonNegativeInteger,
normalizedText,
requireRcon,
transport,
executeModerationAction,
logStaffActivity,
runExternalWithAudit,
finalizeExternalWithAudit,
} = dependencies;
function moderationAction(data: Record<string, unknown>): ModerationAction {
const action = normalizedText(data.action, 32);
if (action === "kick" || action === "unmute") {
return { action, userId: positiveInteger(data.userId) };
}
if (action === "mute") {
const duration = nonNegativeInteger(data.duration);
if (duration > 525_600) {
throw failure("VALIDATION", "errors.housekeeping.validation");
}
return { action, userId: positiveInteger(data.userId), duration };
}
if (action === "alert") {
return {
action,
userId: positiveInteger(data.userId),
message: normalizedText(data.message, 500),
};
}
if (action === "room-kick") {
return { action, roomId: positiveInteger(data.roomId) };
}
if (action === "broadcast") {
const type = normalizedText(data.type, 16);
if (type !== "hotel" && type !== "staff") {
throw failure("VALIDATION", "errors.housekeeping.validation");
}
return {
action,
type,
message: normalizedText(data.message, 500),
};
}
throw failure("VALIDATION", "errors.housekeeping.validation");
}
function actionTarget(input: ModerationAction): {
target: string;
targetId: number | undefined;
} {
if ("userId" in input) return { target: "User", targetId: input.userId };
if ("roomId" in input) return { target: "Room", targetId: input.roomId };
return { target: "broadcast", targetId: undefined };
}
async function deliverModerationAction(
input: ModerationAction,
context: PeopleModerationMutationContext,
): Promise<void> {
const delivered = await executeModerationAction(transport, input);
if (!context.legacy) await requireRcon(delivered);
}
async function executeModerationMutation(
input: unknown,
context: PeopleModerationMutationContext,
): Promise<PeopleModerationMutationSnapshot> {
const action = moderationAction(record(input));
const target = actionTarget(action);
const snapshot = {
before: null,
after: { ...action },
} satisfies PeopleModerationMutationSnapshot;
return runExternalWithAudit(
context,
"moderation.action",
target.target,
target.targetId,
snapshot,
() => deliverModerationAction(action, context),
{ before: null, after: null },
);
}
async function executeCfhSanction(
input: unknown,
context: PeopleModerationMutationContext,
): Promise<PeopleModerationMutationSnapshot> {
const data = record(input);
const ticketId = positiveInteger(data.ticketId);
const action = moderationAction({
...data,
message: data.message ?? data.reason,
});
const reason = normalizedText(data.reason, 500);
let snapshot!: PeopleModerationMutationSnapshot;
await db.transaction(async (tx) => {
const [ticket] = await tx
.select({
id: SupportTickets.id,
state: SupportTickets.state,
modId: SupportTickets.modId,
})
.from(SupportTickets)
.where(eq(SupportTickets.id, ticketId))
.limit(1);
if (!ticket) {
throw failure("NOT_FOUND", "errors.housekeeping.notFound");
}
snapshot = {
before: {
id: ticket.id,
state: ticket.state,
moderatorId: ticket.modId,
},
after: {
id: ticket.id,
state: 2,
moderatorId: context.capability.actor.id,
sanction: action.action,
reason,
},
};
await tx
.update(SupportTickets)
.set({ state: 2, modId: context.capability.actor.id })
.where(eq(SupportTickets.id, ticketId));
await writeAudit(
auditEntry(
context,
"cfh.sanction",
"support_tickets",
ticketId,
snapshot,
"intent",
),
tx,
);
});
return finalizeExternalWithAudit(
context,
"cfh.sanction",
"support_tickets",
ticketId,
snapshot,
() => deliverModerationAction(action, context),
);
}
async function executeBanMutation(
operation: Extract<PeopleModerationMutationOperation, `ban.${string}`>,
input: unknown,
context: PeopleModerationMutationContext,
): Promise<PeopleModerationMutationSnapshot> {
const data = record(input);
if (operation === "ban.create") {
const userId = positiveInteger(data.userId);
const hours = nonNegativeInteger(data.hours);
const type = normalizedText(data.type, 16);
const reason = normalizedText(data.reason, 500);
if (
hours > 876_000 ||
!["account", "ip", "machine", "super"].includes(type)
) {
throw failure("VALIDATION", "errors.housekeeping.validation");
}
const now = Math.floor(Date.now() / 1_000);
const banExpire = hours > 0 ? now + hours * 3_600 : 0;
let username: string | null = null;
let banId = 0;
let snapshot!: PeopleModerationMutationSnapshot;
await db.transaction(async (tx) => {
const [user] = await tx
.select({ username: User.username })
.from(User)
.where(eq(User.id, userId))
.limit(1);
username = user?.username ?? null;
const [result] = await tx.insert(Ban).values({
userId,
ip: "",
machineId: "",
userStaffId: context.capability.actor.id,
timestamp: now,
banExpire,
banReason: reason,
type: type as "account" | "ip" | "machine" | "super",
cfhTopic: -1,
});
banId = positiveInteger(result.insertId);
snapshot = {
before: null,
after: {
id: banId,
userId,
type,
reason,
expiresAt: banExpire,
},
};
await writeAudit(
auditEntry(context, operation, "Ban", banId, snapshot, "intent"),
tx,
);
});
return finalizeExternalWithAudit(
context,
operation,
"Ban",
banId,
snapshot,
async () => {
let delivered = true;
if (username !== null) {
delivered = await transport.disconnectUser(userId, username);
}
await logStaffActivity({
staffId: context.capability.actor.id,
action: "user_ban",
description: `Banned user #${userId} (${type}, ${
hours > 0 ? `${hours}h` : "permanent"
}): ${reason}`,
targetType: "user",
targetId: userId,
});
await requireRcon(delivered);
},
);
}
const id = positiveInteger(data.id);
let snapshot!: PeopleModerationMutationSnapshot;
await db.transaction(async (tx) => {
const [existing] = await tx
.select({
id: Ban.id,
userId: Ban.userId,
reason: Ban.banReason,
type: Ban.type,
})
.from(Ban)
.where(eq(Ban.id, id))
.limit(1);
await tx.delete(Ban).where(eq(Ban.id, id));
snapshot = { before: existing ?? null, after: null };
await writeAudit(
auditEntry(context, operation, "Ban", id, snapshot, "intent"),
tx,
);
});
return finalizeExternalWithAudit(
context,
operation,
"Ban",
id,
snapshot,
() =>
logStaffActivity({
staffId: context.capability.actor.id,
action: "ban_lift",
description: `Lifted ban #${id}`,
}),
);
}
return {
execute(operation, input, context) {
if (operation === "moderation.action") {
return executeModerationMutation(input, context);
}
if (operation === "cfh.sanction") {
return executeCfhSanction(input, context);
}
if (operation === "ban.create" || operation === "ban.lift") {
return executeBanMutation(operation, input, context);
}
throw failure("VALIDATION", "errors.housekeeping.validation");
},
};
}
@@ -1,150 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import type { AuditEntry } from "@/lib/services/audit";
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
const { alertUser, audit, resolveServerContext } = vi.hoisted(() => ({
alertUser: vi.fn(),
audit: vi.fn(),
resolveServerContext: vi.fn(),
}));
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
getHousekeepingCapabilityContext: resolveServerContext,
}));
vi.mock("@/lib/auth", () => ({ invalidateLoginCache: vi.fn() }));
vi.mock("@/lib/auth/password", () => ({ hashPassword: vi.fn() }));
vi.mock("@/lib/db", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/db")>()),
db: {
select: vi.fn(() => {
throw new Error("alert must not query DB");
}),
},
}));
vi.mock("@/lib/services/audit", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/services/audit")>()),
logAudit: audit,
}));
vi.mock("@/lib/services/rcon", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/services/rcon")>()),
rcon: { alertUser },
}));
vi.mock("@/lib/services/webhook", () => ({ notify: vi.fn() }));
import { peopleMutationService } from "./mutations";
function context(): HousekeepingCapabilityContext {
return {
actor: { id: 42, username: "operator", rank: 6 },
isSuperAdmin: false,
has: (slug) => slug === PERMS.USERS_EDIT,
hasAny: (...slugs) => slugs.includes(PERMS.USERS_EDIT),
hasAll: (...slugs) => slugs.every((slug) => slug === PERMS.USERS_EDIT),
};
}
const invocation = {
correlationId: "audit-contract",
expectedActorId: 42,
};
beforeEach(() => {
vi.clearAllMocks();
resolveServerContext.mockResolvedValue(context());
alertUser.mockResolvedValue(true);
audit.mockResolvedValue(undefined);
});
describe("People external audit contract", () => {
it("blocks the external operation when intent persistence fails", async () => {
audit.mockRejectedValueOnce(new Error("intent unavailable"));
const result = await peopleMutationService.execute(
invocation,
"user.alert",
{ userId: 7, message: "Hello" },
);
expect(result).toMatchObject({ ok: false });
expect(alertUser).not.toHaveBeenCalled();
expect(audit).toHaveBeenCalledWith(
expect.objectContaining({ outcome: "intent" }),
);
});
it("persists a correlated failure outcome when a pre-mutation external operation fails", async () => {
alertUser.mockResolvedValue(false);
const result = await peopleMutationService.execute(
invocation,
"user.alert",
{ userId: 7, message: "Hello" },
);
expect(result).toMatchObject({
ok: false,
error: { code: "DEPENDENCY_UNAVAILABLE" },
});
expect(
audit.mock.calls.map((call) => {
const entry = call[0] as AuditEntry;
return {
outcome: entry.outcome,
correlationId: entry.correlationId,
};
}),
).toEqual([
{ outcome: "intent", correlationId: "audit-contract" },
{ outcome: "failure", correlationId: "audit-contract" },
]);
});
it("returns serializable partial completion when final outcome persistence fails", async () => {
audit.mockImplementation(async (entry: AuditEntry) => {
if (entry.outcome === "success") throw new Error("outcome unavailable");
});
const result = await peopleMutationService.execute(
invocation,
"user.alert",
{ userId: 7, message: "Hello" },
);
expect(result).toMatchObject({
ok: true,
completion: {
status: "partial",
external: "completed",
audit: "persisted",
},
data: {
before: null,
after: { userId: 7, alertDelivered: true },
},
correlationId: "audit-contract",
});
expect(() => JSON.stringify(result)).not.toThrow();
expect(alertUser).toHaveBeenCalledTimes(1);
expect(
audit.mock.calls.map((call) => (call[0] as AuditEntry).outcome),
).toEqual(["intent", "success", "partial"]);
});
it("attempts legacy alert without a target lookup when the database is unavailable", async () => {
const result = await peopleMutationService.execute(
{ ...invocation, legacy: true },
"user.alert",
{ userId: 900719925, message: "Direct RCON" },
);
expect(result).toMatchObject({
ok: true,
data: {
before: null,
after: { userId: 900719925, alertDelivered: true },
},
});
expect(alertUser).toHaveBeenCalledWith(900719925, "Direct RCON");
});
});
@@ -1,134 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
const { audit, alertUser, resolveServerContext, selectLimit } = vi.hoisted(
() => ({
audit: vi.fn(),
alertUser: vi.fn(),
resolveServerContext: vi.fn(),
selectLimit: vi.fn(),
}),
);
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
getHousekeepingCapabilityContext: resolveServerContext,
}));
vi.mock("@/lib/auth", () => ({ invalidateLoginCache: vi.fn() }));
vi.mock("@/lib/auth/password", () => ({ hashPassword: vi.fn() }));
vi.mock("@/lib/db", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/db")>();
return {
...actual,
db: {
...actual.db,
select: vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => ({ limit: selectLimit })),
})),
})),
},
};
});
vi.mock("@/lib/services/audit", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/services/audit")>()),
logAudit: audit,
}));
vi.mock("@/lib/services/rcon", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/services/rcon")>()),
rcon: { alertUser },
}));
vi.mock("@/lib/services/webhook", () => ({ notify: vi.fn() }));
import { peopleMutationService } from "./mutations";
function capabilityContext(
granted: readonly string[],
): HousekeepingCapabilityContext {
const permissions = new Set(granted);
return {
actor: { id: 42, username: "operator", rank: 6 },
isSuperAdmin: false,
has: (slug) => permissions.has(slug),
hasAny: (...slugs) => slugs.some((slug) => permissions.has(slug)),
hasAll: (...slugs) => slugs.every((slug) => permissions.has(slug)),
};
}
const target = {
id: 7,
username: "Alice",
mail: "[email protected]",
rank: 2,
motto: "Ready",
credits: 100,
pixels: 50,
online: "1",
};
beforeEach(() => {
vi.clearAllMocks();
resolveServerContext.mockResolvedValue(capabilityContext([]));
selectLimit.mockResolvedValue([target]);
alertUser.mockResolvedValue(true);
audit.mockResolvedValue(undefined);
});
describe("People production mutation boundary", () => {
it("rechecks authorization before any production adapter work", async () => {
const result = await peopleMutationService.execute(
{ expectedActorId: 42, correlationId: "production-denied" },
"user.alert",
{ userId: 7, message: "Hello" },
);
expect(result).toMatchObject({ ok: false, error: { code: "FORBIDDEN" } });
expect(selectLimit).not.toHaveBeenCalled();
expect(alertUser).not.toHaveBeenCalled();
expect(audit).not.toHaveBeenCalled();
});
it("emits sanitized before and after evidence for a successful mutation", async () => {
resolveServerContext.mockResolvedValue(
capabilityContext([PERMS.USERS_EDIT]),
);
const result = await peopleMutationService.execute(
{ expectedActorId: 42, correlationId: "production-alert" },
"user.alert",
{ userId: 7, message: "Hello" },
);
expect(result).toMatchObject({
ok: true,
data: {
before: null,
after: { userId: 7, alertDelivered: true },
},
});
expect(audit).toHaveBeenCalledWith(
expect.objectContaining({
action: "people.user.alert",
before: undefined,
after: expect.objectContaining({ alertDelivered: true }),
correlationId: "production-alert",
outcome: "success",
}),
);
});
it("blocks the external action when required intent evidence cannot be persisted", async () => {
resolveServerContext.mockResolvedValue(
capabilityContext([PERMS.USERS_EDIT]),
);
audit.mockRejectedValue(new Error("audit database unavailable"));
const result = await peopleMutationService.execute(
{ expectedActorId: 42, correlationId: "production-audit-failure" },
"user.alert",
{ userId: 7, message: "Hello" },
);
expect(alertUser).not.toHaveBeenCalled();
expect(result).toMatchObject({
ok: false,
error: { code: "DEPENDENCY_UNAVAILABLE" },
correlationId: "production-audit-failure",
});
});
});
@@ -1,152 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
const {
audit,
disconnectUser,
insertValues,
resolveServerContext,
selectLimit,
transaction,
txSelectLimit,
txSelectOrderBy,
} = vi.hoisted(() => ({
audit: vi.fn(),
disconnectUser: vi.fn(),
insertValues: vi.fn(),
resolveServerContext: vi.fn(),
selectLimit: vi.fn(),
transaction: vi.fn(),
txSelectLimit: vi.fn(),
txSelectOrderBy: vi.fn(),
}));
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
getHousekeepingCapabilityContext: resolveServerContext,
}));
vi.mock("@/lib/auth", () => ({ invalidateLoginCache: vi.fn() }));
vi.mock("@/lib/auth/password", () => ({ hashPassword: vi.fn() }));
vi.mock("@/lib/db", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/db")>();
const tx = {
select: vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => ({ orderBy: txSelectOrderBy })),
})),
})),
insert: vi.fn(() => ({ values: insertValues })),
};
return {
...actual,
db: {
...actual.db,
select: vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => ({ limit: selectLimit })),
})),
})),
transaction: transaction.mockImplementation(async (callback) =>
callback(tx),
),
},
};
});
vi.mock("@/lib/services/audit", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/services/audit")>()),
logAudit: audit,
}));
vi.mock("@/lib/services/rcon", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/services/rcon")>()),
rcon: { disconnectUser },
}));
vi.mock("@/lib/services/staff-activity", () => ({ logStaffActivity: vi.fn() }));
vi.mock("@/lib/services/webhook", () => ({ notify: vi.fn() }));
import { peopleMutationService } from "./mutations";
function capabilityContext(): HousekeepingCapabilityContext {
return {
actor: { id: 42, username: "operator", rank: 6 },
isSuperAdmin: false,
has: (slug) => slug === PERMS.USERS_BAN,
hasAny: (...slugs) => slugs.includes(PERMS.USERS_BAN),
hasAll: (...slugs) => slugs.every((slug) => slug === PERMS.USERS_BAN),
};
}
beforeEach(() => {
vi.clearAllMocks();
resolveServerContext.mockResolvedValue(capabilityContext());
selectLimit.mockResolvedValue([
{
id: 7,
username: "Alice",
rank: 2,
motto: "Ready",
credits: 100,
pixels: 50,
online: "1",
},
]);
txSelectLimit
.mockResolvedValueOnce([
{
id: 88,
banExpire: 0,
banReason: "Permanent active ban",
type: "account",
},
])
.mockResolvedValue([
{
id: 89,
banExpire: Math.floor(Date.now() / 1000) + 24 * 3600,
banReason: "Repeated harassment",
type: "account",
},
]);
txSelectOrderBy.mockReturnValue({ limit: txSelectLimit });
insertValues.mockResolvedValue([{}]);
disconnectUser.mockResolvedValue(true);
audit.mockResolvedValue(undefined);
});
describe("People production reason audit", () => {
it("persists the nonblank sanction reason with observed before and after evidence", async () => {
const result = await peopleMutationService.execute(
{ expectedActorId: 42, correlationId: "production-ban" },
"user.ban",
{
userId: 7,
reason: "Repeated harassment",
duration: 24,
type: "account",
},
);
expect(result).toMatchObject({ ok: true });
expect(transaction).toHaveBeenCalledTimes(1);
expect(audit).toHaveBeenCalledWith(
expect.objectContaining({
action: "people.user.ban",
outcome: "intent",
before: expect.objectContaining({
id: 7,
banned: true,
banExpire: 0,
reason: "Permanent active ban",
}),
after: expect.objectContaining({
id: 7,
banned: true,
reason: "Repeated harassment",
}),
}),
expect.anything(),
);
expect(txSelectOrderBy).toHaveBeenCalledTimes(2);
expect(audit.mock.calls[0]?.[0]?.before).not.toHaveProperty("mail");
expect(audit.mock.calls[0]?.[0]?.after).not.toHaveProperty("mail");
});
});
@@ -1,116 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
const { alertUser, audit, resolveServerContext, selectLimit } = vi.hoisted(
() => ({
alertUser: vi.fn(),
audit: vi.fn(),
resolveServerContext: vi.fn(),
selectLimit: vi.fn(),
}),
);
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
getHousekeepingCapabilityContext: resolveServerContext,
}));
vi.mock("@/lib/auth", () => ({ invalidateLoginCache: vi.fn() }));
vi.mock("@/lib/auth/password", () => ({ hashPassword: vi.fn() }));
vi.mock("@/lib/db", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/db")>();
return {
...actual,
db: {
...actual.db,
select: vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => ({ limit: selectLimit })),
})),
})),
},
};
});
vi.mock("@/lib/services/audit", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/services/audit")>()),
logAudit: audit,
}));
vi.mock("@/lib/services/rcon", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/services/rcon")>()),
rcon: { alertUser },
}));
vi.mock("@/lib/services/webhook", () => ({ notify: vi.fn() }));
import { peopleMutationService } from "./mutations";
function context(granted: readonly string[]): HousekeepingCapabilityContext {
const permissions = new Set(granted);
return {
actor: { id: 42, username: "operator", rank: 6 },
isSuperAdmin: false,
has: (slug) => permissions.has(slug),
hasAny: (...slugs) => slugs.some((slug) => permissions.has(slug)),
hasAll: (...slugs) => slugs.every((slug) => permissions.has(slug)),
};
}
beforeEach(() => {
vi.clearAllMocks();
resolveServerContext.mockResolvedValue(context([]));
selectLimit.mockResolvedValue([
{
id: 7,
username: "Alice",
rank: 2,
motto: "Ready",
credits: 100,
pixels: 50,
online: "1",
},
]);
alertUser.mockResolvedValue(true);
audit.mockResolvedValue(undefined);
});
describe("People production server authority", () => {
it("rejects a caller-forged capability and resolves the authenticated request actor", async () => {
const result = await peopleMutationService.execute(
{
correlationId: "server-authority",
expectedActorId: 42,
capability: context([PERMS.USERS_EDIT]),
} as never,
"user.alert",
{ userId: 7, message: "Hello" },
);
expect(resolveServerContext).toHaveBeenCalledTimes(1);
expect(result).toMatchObject({
ok: false,
error: { code: "FORBIDDEN" },
correlationId: "server-authority",
});
expect(selectLimit).not.toHaveBeenCalled();
expect(alertUser).not.toHaveBeenCalled();
expect(audit).not.toHaveBeenCalled();
});
it("blocks Task 13 moderation work before intent audit or transport on actor mismatch", async () => {
resolveServerContext.mockResolvedValue(context([PERMS.MOD_ACTIONS]));
const result = await peopleMutationService.execute(
{
correlationId: "task13-server-authority",
expectedActorId: 99,
legacy: true,
},
"moderation.action",
{ action: "alert", userId: 7, message: "Stop" },
);
expect(result).toMatchObject({
ok: false,
error: { code: "FORBIDDEN" },
correlationId: "task13-server-authority",
});
expect(alertUser).not.toHaveBeenCalled();
expect(audit).not.toHaveBeenCalled();
});
});
@@ -1,118 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
const {
audit,
globalDeleteWhere,
resolveServerContext,
transaction,
txDeleteWhere,
txSelectLimit,
} = vi.hoisted(() => ({
audit: vi.fn(),
globalDeleteWhere: vi.fn(),
resolveServerContext: vi.fn(),
transaction: vi.fn(),
txDeleteWhere: vi.fn(),
txSelectLimit: vi.fn(),
}));
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
getHousekeepingCapabilityContext: resolveServerContext,
}));
vi.mock("@/lib/auth", () => ({ invalidateLoginCache: vi.fn() }));
vi.mock("@/lib/auth/password", () => ({ hashPassword: vi.fn() }));
vi.mock("@/lib/db", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/db")>();
return {
...actual,
db: {
...actual.db,
select: vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => ({ limit: txSelectLimit })),
})),
})),
delete: vi.fn(() => ({ where: globalDeleteWhere })),
transaction,
},
};
});
vi.mock("@/lib/services/audit", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/services/audit")>()),
logAudit: audit,
}));
vi.mock("@/lib/services/webhook", () => ({ notify: vi.fn() }));
import { peopleMutationService } from "./mutations";
function context(): HousekeepingCapabilityContext {
return {
actor: { id: 42, username: "operator", rank: 6 },
isSuperAdmin: false,
has: (slug) => slug === PERMS.USERS_EDIT,
hasAny: (...slugs) => slugs.includes(PERMS.USERS_EDIT),
hasAll: (...slugs) => slugs.every((slug) => slug === PERMS.USERS_EDIT),
};
}
beforeEach(() => {
vi.clearAllMocks();
resolveServerContext.mockResolvedValue(context());
txSelectLimit.mockResolvedValue([
{ id: 9n, userId: 7, rankId: 3, content: "Application" },
]);
globalDeleteWhere.mockResolvedValue(undefined);
txDeleteWhere.mockResolvedValue(undefined);
transaction.mockImplementation(async (callback) => {
let staged = false;
const tx = {
select: vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => ({ limit: txSelectLimit })),
})),
})),
delete: vi.fn(() => ({
where: vi.fn(async (...args) => {
staged = true;
return txDeleteWhere(...args);
}),
})),
insert: vi.fn(),
};
try {
const value = await callback(tx);
return { value, committed: staged };
} catch (error) {
throw Object.assign(error as Error, { rolledBack: staged });
}
});
audit.mockRejectedValue(new Error("audit unavailable"));
});
describe("People transactional database audit", () => {
it("rolls back application deletion when canonical audit persistence fails", async () => {
const result = await peopleMutationService.execute(
{ correlationId: "transactional-audit", expectedActorId: 42 },
"application.decide",
{ applicationId: 9, decision: "dismiss" },
);
expect(result).toMatchObject({
ok: false,
error: { code: "DEPENDENCY_UNAVAILABLE" },
});
expect(transaction).toHaveBeenCalledTimes(1);
expect(globalDeleteWhere).not.toHaveBeenCalled();
expect(txDeleteWhere).toHaveBeenCalledTimes(1);
expect(audit).toHaveBeenCalledWith(
expect.objectContaining({
outcome: "success",
before: expect.objectContaining({ id: "9" }),
after: undefined,
}),
expect.objectContaining({ insert: expect.any(Function) }),
);
});
});
File diff suppressed because it is too large. Load diff
@@ -1,480 +0,0 @@
import "server-only";
import { eq } from "drizzle-orm";
import {
Ban,
type Db,
SupportTickets,
WebsiteHelpCenterTicketReplies,
WebsiteHelpCenterTickets,
WebsiteTicket,
WebsiteTicketMessage,
WebsiteTicketTemplate,
} from "@/lib/db";
import type { AuditEntry, HousekeepingAuditWriter } from "@/lib/services/audit";
import type {
HousekeepingCapabilityContext,
HousekeepingErrorCode,
HousekeepingPartialCompletion,
} from "../../../foundation/contracts";
export const PEOPLE_SUPPORT_MUTATION_OPERATIONS = [
"ticket.reply",
"ticket.assign",
"ticket.status",
"ticket.priority",
"ticket-template.change",
"help-ticket.reply",
"help-ticket.status",
"help-ticket.unban",
"cfh.resolve",
] as const;
export type PeopleSupportMutationOperation =
(typeof PEOPLE_SUPPORT_MUTATION_OPERATIONS)[number];
export interface PeopleSupportMutationContext {
readonly capability: HousekeepingCapabilityContext;
readonly correlationId: string;
readonly legacy: boolean;
}
export interface PeopleSupportMutationSnapshot {
readonly before: Readonly<Record<string, unknown>> | null;
readonly after: Readonly<Record<string, unknown>> | null;
readonly output?: Readonly<Record<string, unknown>>;
readonly completion?: HousekeepingPartialCompletion;
}
type AuditOutcome = "intent" | "success" | "failure" | "partial";
export interface PeopleSupportMutationDependencies {
readonly db: Db;
readonly writeAudit: HousekeepingAuditWriter["write"];
readonly auditEntry: (
context: PeopleSupportMutationContext,
operation: PeopleSupportMutationOperation,
target: string,
targetId: number | undefined,
snapshot: PeopleSupportMutationSnapshot,
outcome: AuditOutcome,
) => AuditEntry;
readonly failure: (code: HousekeepingErrorCode, messageKey: string) => Error;
readonly record: (input: unknown) => Record<string, unknown>;
readonly positiveInteger: (value: unknown) => number;
readonly nonNegativeInteger: (value: unknown) => number;
readonly normalizedText: (
value: unknown,
max: number,
required?: boolean,
) => string;
readonly canonicalTicketId: (value: string | number | bigint) => bigint;
readonly auditTargetId: (value: bigint) => number | undefined;
}
export interface PeopleSupportMutationExecutor {
execute(
operation: PeopleSupportMutationOperation,
input: unknown,
context: PeopleSupportMutationContext,
): Promise<PeopleSupportMutationSnapshot>;
}
export function isPeopleSupportMutationOperation(
operation: string,
): operation is PeopleSupportMutationOperation {
return (PEOPLE_SUPPORT_MUTATION_OPERATIONS as readonly string[]).includes(
operation,
);
}
export function createPeopleSupportMutationExecutor(
dependencies: PeopleSupportMutationDependencies,
): PeopleSupportMutationExecutor {
const {
db,
writeAudit,
auditEntry,
failure,
record,
positiveInteger,
nonNegativeInteger,
normalizedText,
canonicalTicketId,
auditTargetId,
} = dependencies;
async function executeTicketMutation(
operation: Extract<PeopleSupportMutationOperation, `ticket.${string}`>,
input: unknown,
context: PeopleSupportMutationContext,
): Promise<PeopleSupportMutationSnapshot> {
const data = record(input);
const ticketId = positiveInteger(data.ticketId);
let snapshot!: PeopleSupportMutationSnapshot;
await db.transaction(async (tx) => {
const [ticket] = await tx
.select({
id: WebsiteTicket.id,
assigneeId: WebsiteTicket.assigneeId,
status: WebsiteTicket.status,
priority: WebsiteTicket.priority,
})
.from(WebsiteTicket)
.where(eq(WebsiteTicket.id, ticketId))
.limit(1);
if (!ticket) {
throw failure("NOT_FOUND", "errors.housekeeping.notFound");
}
const before = {
id: ticket.id,
assigneeId: ticket.assigneeId,
status: ticket.status,
priority: ticket.priority,
};
const now = new Date();
if (operation === "ticket.reply") {
const message = normalizedText(data.message, 5_000);
await tx.insert(WebsiteTicketMessage).values({
ticketId,
userId: context.capability.actor.id,
message,
isStaff: 1,
});
const assigneeId = ticket.assigneeId ?? context.capability.actor.id;
await tx
.update(WebsiteTicket)
.set({ status: "waiting", assigneeId, updatedAt: now })
.where(eq(WebsiteTicket.id, ticketId));
snapshot = {
before,
after: { ...before, assigneeId, status: "waiting" },
};
} else if (operation === "ticket.assign") {
const assigneeId =
data.assigneeId === null ? null : positiveInteger(data.assigneeId);
const status = assigneeId === null ? "open" : "in_progress";
await tx
.update(WebsiteTicket)
.set({ assigneeId, status, updatedAt: now })
.where(eq(WebsiteTicket.id, ticketId));
snapshot = { before, after: { ...before, assigneeId, status } };
} else if (operation === "ticket.status") {
const status = normalizedText(data.status, 32);
if (!["open", "in_progress", "waiting", "closed"].includes(status)) {
throw failure("VALIDATION", "errors.housekeeping.validation");
}
const assigneeId =
status === "in_progress" && ticket.assigneeId === null
? context.capability.actor.id
: ticket.assigneeId;
await tx
.update(WebsiteTicket)
.set({
status,
assigneeId,
updatedAt: now,
closedAt: status === "closed" ? now : null,
})
.where(eq(WebsiteTicket.id, ticketId));
snapshot = { before, after: { ...before, assigneeId, status } };
} else {
const priority = normalizedText(data.priority, 32);
if (!["low", "normal", "high", "urgent"].includes(priority)) {
throw failure("VALIDATION", "errors.housekeeping.validation");
}
await tx
.update(WebsiteTicket)
.set({ priority, updatedAt: now })
.where(eq(WebsiteTicket.id, ticketId));
snapshot = { before, after: { ...before, priority } };
}
await writeAudit(
auditEntry(
context,
operation,
"WebsiteTicket",
ticketId,
snapshot,
"success",
),
tx,
);
});
return snapshot;
}
async function executeTicketTemplateChange(
input: unknown,
context: PeopleSupportMutationContext,
): Promise<PeopleSupportMutationSnapshot> {
const data = record(input);
const action = normalizedText(data.action, 16);
let snapshot!: PeopleSupportMutationSnapshot;
await db.transaction(async (tx) => {
let targetId: number | undefined;
if (action === "create") {
const values = {
title: normalizedText(data.title, 255),
content: normalizedText(data.content, 5_000),
category: normalizedText(data.category ?? "general", 50),
sortOrder:
data.sortOrder === undefined
? 0
: nonNegativeInteger(data.sortOrder),
};
const [result] = await tx.insert(WebsiteTicketTemplate).values(values);
targetId = positiveInteger(result.insertId);
snapshot = { before: null, after: { id: targetId, ...values } };
} else {
const id = positiveInteger(data.id);
targetId = id;
const [existing] = await tx
.select({
id: WebsiteTicketTemplate.id,
title: WebsiteTicketTemplate.title,
content: WebsiteTicketTemplate.content,
category: WebsiteTicketTemplate.category,
sortOrder: WebsiteTicketTemplate.sortOrder,
})
.from(WebsiteTicketTemplate)
.where(eq(WebsiteTicketTemplate.id, id))
.limit(1);
if (action === "update" && !existing) {
throw failure("NOT_FOUND", "errors.housekeeping.notFound");
}
if (action === "delete") {
await tx
.delete(WebsiteTicketTemplate)
.where(eq(WebsiteTicketTemplate.id, id));
snapshot = { before: existing ?? null, after: null };
} else if (action === "update") {
const changes = {
...(data.title === undefined
? {}
: { title: normalizedText(data.title, 255) }),
...(data.content === undefined
? {}
: { content: normalizedText(data.content, 5_000) }),
...(data.category === undefined
? {}
: { category: normalizedText(data.category, 50, false) }),
...(data.sortOrder === undefined
? {}
: { sortOrder: nonNegativeInteger(data.sortOrder) }),
};
await tx
.update(WebsiteTicketTemplate)
.set(changes)
.where(eq(WebsiteTicketTemplate.id, id));
snapshot = {
before: existing ?? null,
after: { ...existing, ...changes },
};
} else {
throw failure("VALIDATION", "errors.housekeeping.validation");
}
}
await writeAudit(
auditEntry(
context,
"ticket-template.change",
"WebsiteTicketTemplate",
targetId,
snapshot,
"success",
),
tx,
);
});
return snapshot;
}
async function executeHelpTicketMutation(
operation: Extract<PeopleSupportMutationOperation, `help-ticket.${string}`>,
input: unknown,
context: PeopleSupportMutationContext,
): Promise<PeopleSupportMutationSnapshot> {
const data = record(input);
let ticketId: bigint;
try {
ticketId = canonicalTicketId(data.ticketId as string | number | bigint);
} catch {
throw failure("VALIDATION", "errors.housekeeping.validation");
}
let snapshot!: PeopleSupportMutationSnapshot;
await db.transaction(async (tx) => {
const [ticket] = await tx
.select({
id: WebsiteHelpCenterTickets.id,
userId: WebsiteHelpCenterTickets.userId,
open: WebsiteHelpCenterTickets.open,
title: WebsiteHelpCenterTickets.title,
})
.from(WebsiteHelpCenterTickets)
.where(eq(WebsiteHelpCenterTickets.id, ticketId))
.limit(1);
if (!ticket) {
throw failure("NOT_FOUND", "errors.housekeeping.notFound");
}
const id = ticketId.toString();
const before = {
id,
userId: ticket.userId,
open: Boolean(ticket.open),
title: ticket.title,
};
const now = new Date();
if (operation === "help-ticket.reply") {
await tx.insert(WebsiteHelpCenterTicketReplies).values({
ticketId,
userId: context.capability.actor.id,
content: normalizedText(data.content, 5_000),
createdAt: now,
updatedAt: now,
});
await tx
.update(WebsiteHelpCenterTickets)
.set({ updatedAt: now })
.where(eq(WebsiteHelpCenterTickets.id, ticketId));
snapshot = { before, after: before };
} else if (operation === "help-ticket.status") {
const status = normalizedText(data.status, 16);
const open =
status === "reopen" ? true : status === "close" ? false : null;
if (open === null) {
throw failure("VALIDATION", "errors.housekeeping.validation");
}
if (Boolean(ticket.open) === open) {
throw failure("CONFLICT", "errors.housekeeping.conflict");
}
await tx
.update(WebsiteHelpCenterTickets)
.set({ open, updatedAt: now })
.where(eq(WebsiteHelpCenterTickets.id, ticketId));
snapshot = { before, after: { ...before, open } };
} else {
if (ticket.userId === null) {
throw failure("CONFLICT", "errors.housekeeping.conflict");
}
const deleted = await tx
.delete(Ban)
.where(eq(Ban.userId, ticket.userId));
const removed = Number(
(deleted as unknown as [{ affectedRows: number }])[0]?.affectedRows ??
0,
);
if (ticket.open) {
await tx
.update(WebsiteHelpCenterTickets)
.set({ open: false, updatedAt: now })
.where(eq(WebsiteHelpCenterTickets.id, ticketId));
}
snapshot = {
before,
after: { ...before, open: false, removedBans: removed },
output: { removed, userId: ticket.userId },
};
}
await writeAudit(
auditEntry(
context,
operation,
"WebsiteHelpCenterTickets",
auditTargetId(ticketId),
snapshot,
"success",
),
tx,
);
});
return snapshot;
}
async function executeCfhResolve(
input: unknown,
context: PeopleSupportMutationContext,
): Promise<PeopleSupportMutationSnapshot> {
const data = record(input);
const ticketId = positiveInteger(data.ticketId);
const state = nonNegativeInteger(data.state);
if (state > 3) {
throw failure("VALIDATION", "errors.housekeeping.validation");
}
let snapshot!: PeopleSupportMutationSnapshot;
await db.transaction(async (tx) => {
const [ticket] = await tx
.select({
id: SupportTickets.id,
state: SupportTickets.state,
modId: SupportTickets.modId,
})
.from(SupportTickets)
.where(eq(SupportTickets.id, ticketId))
.limit(1);
if (!ticket) {
throw failure("NOT_FOUND", "errors.housekeeping.notFound");
}
snapshot = {
before: {
id: ticket.id,
state: ticket.state,
moderatorId: ticket.modId,
},
after: {
id: ticket.id,
state,
moderatorId: context.capability.actor.id,
},
};
await tx
.update(SupportTickets)
.set({ state, modId: context.capability.actor.id })
.where(eq(SupportTickets.id, ticketId));
await writeAudit(
auditEntry(
context,
"cfh.resolve",
"support_tickets",
ticketId,
snapshot,
"success",
),
tx,
);
});
return snapshot;
}
return {
async execute(operation, input, context) {
if (operation.startsWith("ticket.")) {
return executeTicketMutation(
operation as Extract<
PeopleSupportMutationOperation,
`ticket.${string}`
>,
input,
context,
);
}
if (operation === "ticket-template.change") {
return executeTicketTemplateChange(input, context);
}
if (operation.startsWith("help-ticket.")) {
return executeHelpTicketMutation(
operation as Extract<
PeopleSupportMutationOperation,
`help-ticket.${string}`
>,
input,
context,
);
}
if (operation === "cfh.resolve") {
return executeCfhResolve(input, context);
}
throw failure("VALIDATION", "errors.housekeeping.validation");
},
};
}