Files
Dune/lib/admin-removal.ts
T
Quintine e918e2e0c0 Add reversible administrator room archiving
Organize closed rooms with archive filters, live-authorized atomic receipts and exact saved retries. Preserve saved games, timestamps, credentials, discussion and AI deadlines. Unarchive leaves play closed; reopen requires unarchive, and removal/restoration preserves both flags.

Verified types, lint, 5379 offline tests, 52 HTTP tests and build. Twenty dedicated administrator HTTP groups and independent privacy/persistence review passed. Desktop/mobile browser QA confirmed a single archive after a lost response and refresh, readable recovery text, mobile unarchive and separate reopening, and saved-seat restoration. All 2,049 baseline rooms, 4,348 seats and related records preserved. Authenticated production admin acceptance awaits authorized QA access; full admin and rules gates remain open.
2026-09-22 12:55:44 +10:00

65 lines
1.7 KiB
TypeScript

export type AdminRemovalInput = {
operationId: string;
expectedVersion: number;
expectedRevision: number;
removed: boolean;
reason: string;
};
export type AdminRemovalView = {
archived?: true;
closed?: true;
code: string;
version: number;
removed: boolean;
revision: number;
removedAt: number | null;
updatedAt: number | null;
paused: boolean;
joinLocked: boolean;
};
export type AdminRemovalResult = {
operationId: string;
replayed: boolean;
appliedRevision: number;
appliedVersion: number;
room: AdminRemovalView;
};
export function validAdminRemovalInput(
value: unknown,
): value is AdminRemovalInput {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
const input = value as Record<string, unknown>;
const fields = [
'operationId',
'expectedVersion',
'expectedRevision',
'removed',
'reason',
];
return (
Object.keys(input).length === fields.length &&
fields.every((field) => Object.hasOwn(input, field)) &&
typeof input.operationId === 'string' &&
input.operationId.length === 36 &&
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(
input.operationId,
) &&
[input.expectedVersion, input.expectedRevision].every(
(version) =>
Number.isSafeInteger(version) &&
Number(version) >= 0 &&
Number(version) < Number.MAX_SAFE_INTEGER,
) &&
typeof input.removed === 'boolean' &&
typeof input.reason === 'string' &&
!!input.reason.trim() &&
input.reason.length <= 300 &&
!input.reason
.split('')
.some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127)
);
}