Enable an existing AI difficulty for a human seat through admin controls without granting private access or advancing paused play. Preserve custody, credentials, takeback and separate paced resume. Bind atomic writes and outgoing-grant revocation to live authority, room/pause versions and active seat access with durable exact receipts. Validation: types/lint, 5408 offline tests, build and 52 HTTP tests passed on source fingerprint 2157de0f4bb0b74d9760d52102601c52344cf674dee8ccd1d559f0a246a1d4ff; 22 authenticated admin HTTP groups, independent SQL/privacy/UI review, desktop/mobile and refresh checks. All 2098 baseline rooms and 4455 seats preserved. Migration 0015 is additive. Positive production admin QA awaits authorized access; full admin/rules scope remains unfinished.
28 lines
2.0 KiB
TypeScript
28 lines
2.0 KiB
TypeScript
import { DIFFICULTIES, type Difficulty } from '../game/bot-profiles';
|
|
|
|
export type AdminSeatAiInput = {
|
|
operationId: string; expectedVersion: number; expectedControlRevision: number;
|
|
target: string; difficulty: Difficulty; reason: string;
|
|
};
|
|
export type AdminSeatAiView = {
|
|
code: string; version: number; controlRevision: number;
|
|
status: 'lobby' | 'setup' | 'playing' | 'finished' | 'unreadable';
|
|
paused: boolean; closed: boolean; removed: boolean; archived: boolean;
|
|
editable: boolean; blockedReason: string | null;
|
|
players: { id: string; name: string; faction: string; control: 'human' | 'ai' | 'autopilot'; difficulty: Difficulty | null; eligible: boolean }[];
|
|
};
|
|
export type AdminSeatAiResult = { operationId: string; replayed: boolean; appliedVersion: number; room: AdminSeatAiView };
|
|
|
|
const hasControlCharacter = (text: string) => text.split('').some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);
|
|
|
|
export function validAdminSeatAiInput(value: unknown): value is AdminSeatAiInput {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
const input = value as Record<string, unknown>, fields = ['operationId', 'expectedVersion', 'expectedControlRevision', 'target', 'difficulty', 'reason'];
|
|
return Object.keys(input).length === fields.length && fields.every(key => Object.hasOwn(input, key)) &&
|
|
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.expectedControlRevision].every(value => Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) < Number.MAX_SAFE_INTEGER) &&
|
|
typeof input.target === 'string' && input.target.length > 0 && input.target.length <= 80 && input.target.trim() === input.target && !hasControlCharacter(input.target) &&
|
|
DIFFICULTIES.some(difficulty => difficulty === input.difficulty) &&
|
|
typeof input.reason === 'string' && !!input.reason.trim() && input.reason.length <= 300 && !hasControlCharacter(input.reason);
|
|
}
|