Files
Dune/lib/admin-room-creation-client.ts
Quintine 238de3860d Add recoverable administrator room removal and restoration
Preserve room JSON, seats, credentials and messages while removed rooms deny ordinary reads and writes. Add deliberate room-scoped controls, removed-directory filters, persistent player notices and live-authorized atomic version/revision audit receipts. Retain uncertain room/seat requests and hidden recovery drafts through removal; restore the prior pause/join settings without reviving revoked credentials.

Fix AI readiness after a joining human or human faction change so real lobby starts remain legal.

Verified types, lint, 5314 offline tests, 52 HTTP tests and build. Fourteen authenticated HTTP groups and independent privacy/persistence review passed. Desktop/mobile browser QA confirmed a saved removal after a lost response and refresh, one audit on exact retry, persistent notices and recovery draft continuity. All 1,945 baseline rooms, 4,121 seats and related records preserved. Positive authenticated production admin acceptance remains pending authorized QA operator access; full admin and rules gates remain open.
2026-09-22 12:01:22 +10:00

65 lines
4.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { randomId } from './random-id';
import { validAdminRoomCreationInput, type AdminRoomCreationInput, type AdminRoomCreationResult } from './admin-room-creation';
type Storage = Pick<globalThis.Storage, 'getItem' | 'setItem' | 'removeItem'>;
export type AdminRoomCreationRecord = { kind: 'pending'; input: AdminRoomCreationInput } | { kind: 'completed'; result: AdminRoomCreationResult };
const key = (account: string) => `dune.admin-room-creation.v1:${account}`;
const uuid = (value: unknown) => typeof value === 'string' && value.length === 36 && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(value);
export function validAdminRoomCreationResult(value: unknown): value is AdminRoomCreationResult {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
const result = value as Record<string, unknown>;
return Object.keys(result).length === (result.roomRemoved === true ? 6 : 5) &&
(!Object.hasOwn(result, 'roomRemoved') || result.roomRemoved === true && result.hostAccess === false) && typeof result.code === 'string' && result.code.length === 8 && /^[A-Z2-9]{8}$/.test(result.code) &&
uuid(result.hostId) && uuid(result.operationId) && typeof result.replayed === 'boolean' && typeof result.hostAccess === 'boolean';
}
export function newAdminRoomCreationRequest(fields: Omit<AdminRoomCreationInput, 'operationId' | 'sessionToken'>): AdminRoomCreationInput {
const input = { ...fields, name: fields.name.trim(), reason: fields.reason.trim(), operationId: randomId(),
sessionToken: Array.from(globalThis.crypto.getRandomValues(new Uint8Array(32)), byte => byte.toString(16).padStart(2, '0')).join('') };
if (!validAdminRoomCreationInput(input)) throw new Error('Check the host, rules, AI seats and operational reason.');
return input;
}
export function readAdminRoomCreation(storage: Storage, account: string): AdminRoomCreationRecord | null {
const raw = storage.getItem(key(account));
if (raw === null) return null;
let value: unknown;
try { value = raw.length <= 4096 ? JSON.parse(raw) : null; } catch { value = null; }
if (value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 2) {
const record = value as Record<string, unknown>;
if (record.kind === 'pending' && validAdminRoomCreationInput(record.input)) return value as AdminRoomCreationRecord;
if (record.kind === 'completed' && validAdminRoomCreationResult(record.result)) return value as AdminRoomCreationRecord;
}
throw new Error('This tab’s saved room-creation record is unreadable. Keep it until you have checked the room directory; no new request has been sent.');
}
function write(storage: Storage, account: string, record: AdminRoomCreationRecord) {
const text = JSON.stringify(record);
storage.setItem(key(account), text);
if (storage.getItem(key(account)) !== text) throw new Error('The room-creation record could not be saved in this tab.');
}
export function saveAdminRoomCreation(storage: Storage, account: string, input: AdminRoomCreationInput) {
if (!validAdminRoomCreationInput(input)) throw new Error('Invalid room-creation request.');
const previous = readAdminRoomCreation(storage, account);
if (previous && (previous.kind !== 'pending' || JSON.stringify(previous.input) !== JSON.stringify(input)))
throw new Error('Resolve the saved room-creation record before creating another room.');
write(storage, account, { kind: 'pending', input });
}
export function completeAdminRoomCreation(storage: Storage, account: string, input: AdminRoomCreationInput, result: AdminRoomCreationResult) {
if (!validAdminRoomCreationResult(result) || result.operationId !== input.operationId) throw new Error('The server did not confirm this room-creation request.');
if (result.roomRemoved) throw new Error('The room is temporarily removed. Keep the exact creation proof until an administrator restores it.');
const previous = readAdminRoomCreation(storage, account);
if (!previous || previous.kind !== 'pending' || JSON.stringify(previous.input) !== JSON.stringify(input))
throw new Error('The saved room-creation record changed. Keep the current record and inspect the room directory.');
// Replace the private retry proof with a safe room receipt in one storage write.
write(storage, account, { kind: 'completed', result });
}
export function clearAdminRoomCreation(storage: Storage, account: string) {
storage.removeItem(key(account));
if (storage.getItem(key(account)) !== null) throw new Error('The local room-creation record could not be cleared.');
}