import { requestOrigin } from '@/lib/request-origin'; import { authenticate, createSeatHandover, revokeSeatHandover, claimSeatHandover, recoverSeat, SeatControlError, RoomEntryError, setRecoveryKey, setSeatAiDelegate, revokeSeatAiDelegate, useSeatAiDelegate as activateSeatAiDelegate, needsAutomaticRoomRecovery, } from '@/db/rooms'; import { RuleError } from '@/game/engine'; import { validSeatAiDelegateRequest } from '@/lib/seat-ai-delegation'; import { resumeRoom } from '@/db/room-continuation'; import { env, waitUntil } from 'cloudflare:workers'; const noStore = { 'Cache-Control': 'no-store' }; export async function POST(req: Request) { try { const url = new URL(req.url); if (req.headers.get('origin') && req.headers.get('origin') !== requestOrigin(req, env.DUNE_PUBLIC_ORIGIN)) throw new SeatControlError( 'Invalid request origin.', 'INVALID_ORIGIN', 403, ); const code = url.pathname.split('/').at(-2)!.toUpperCase(); if (!/^[A-Z2-9]{8}$/.test(code)) throw new SeatControlError( 'Enter the eight-character room code.', 'INVALID_CONTROL_REQUEST', 400, ); const raw = await req.text(); if (raw.length > 4000) throw new SeatControlError( 'Request too large.', 'REQUEST_TOO_LARGE', 413, ); const body: unknown = JSON.parse(raw); if (!body || typeof body !== 'object' || Array.isArray(body)) throw new SeatControlError( 'Choose a recovery operation.', 'INVALID_CONTROL_REQUEST', 400, ); const input = body as Record; const token = req.headers .get('cookie') ?.split(';') .map((part) => part.trim()) .find((part) => part.startsWith(`dune_${code}=`)) ?.split('=')[1] ?? ''; if ( input.type === 'setSeatAiDelegate' || input.type === 'revokeSeatAiDelegate' || input.type === 'useSeatAiDelegate' ) { if (!validSeatAiDelegateRequest(input)) throw new SeatControlError( 'The AI permission request is invalid.', 'INVALID_CONTROL_REQUEST', 400, ); const auth = await authenticate(code, token); const result = input.type === 'setSeatAiDelegate' ? await setSeatAiDelegate(code, auth, input.version, { grantId: input.grantId, delegateId: input.delegateId, difficulty: input.difficulty, }) : input.type === 'revokeSeatAiDelegate' ? await revokeSeatAiDelegate( code, auth, input.version, input.grantId, ) : await activateSeatAiDelegate(code, auth, input.version, { ownerId: input.ownerId, grantId: input.grantId, }); if (!result.view.roomControl?.paused && !result.view.roomControl?.closed && (result.view.botsPending || needsAutomaticRoomRecovery(result.view))) waitUntil(resumeRoom(code)); return Response.json(result, { headers: noStore }); } if ( input.type === 'setRecoveryKey' || input.type === 'createSeatHandover' || input.type === 'revokeSeatHandover' ) { const auth = await authenticate(code, token); if (input.type === 'createSeatHandover') return Response.json( await createSeatHandover(code, auth, input.version, { offerId: input.offerId, handoverSecret: input.handoverSecret, }), { headers: noStore }, ); if (input.type === 'revokeSeatHandover') return Response.json( await revokeSeatHandover(code, auth, input.version, input.offerId), { headers: noStore }, ); return Response.json( await setRecoveryKey(code, auth, input.version, input.recoverySecret), { headers: noStore }, ); } if (input.type !== 'recoverSeat' && input.type !== 'claimSeatHandover') throw new SeatControlError( 'Choose a recovery operation.', 'INVALID_CONTROL_REQUEST', 400, ); if (input.type === 'claimSeatHandover' && token) { let existing; try { existing = await authenticate(code, token); } catch (error) { if (error instanceof RoomEntryError && error.code === 'ROOM_REMOVED' || !(error instanceof RuleError)) throw error; } if (existing && existing.playerId !== input.playerId) throw new SeatControlError( 'This browser already controls another seat in this room. Use a separate browser profile to accept the handover.', 'ALREADY_SEATED', 409, ); } const result = input.type === 'claimSeatHandover' ? await claimSeatHandover(code, { playerId: input.playerId, offerId: input.offerId, handoverSecret: input.handoverSecret, operationId: input.operationId, newSessionToken: input.newSessionToken, }) : await recoverSeat(code, { playerId: input.playerId, recoverySecret: input.recoverySecret, operationId: input.operationId, newSessionToken: input.newSessionToken, }); return Response.json( { view: result.view, ...('transferred' in result ? { transferred: result.transferred } : { recovered: result.recovered }), replayed: result.replayed, }, { headers: { ...noStore, 'Set-Cookie': `dune_${code}=${result.token}; HttpOnly; SameSite=Strict; Path=/api/rooms/${code}; Max-Age=2592000${requestOrigin(req, env.DUNE_PUBLIC_ORIGIN).startsWith('https:') ? '; Secure' : ''}`, }, }, ); } catch (error) { return Response.json( { error: error instanceof RuleError ? error.message : error instanceof SyntaxError ? 'Invalid JSON.' : 'The server could not complete this seat request.', code: (error instanceof SeatControlError || error instanceof RoomEntryError) ? error.code : error instanceof RuleError ? 'INVALID_SESSION' : error instanceof SyntaxError ? 'INVALID_CONTROL_REQUEST' : 'SERVER_ERROR', }, { status: (error instanceof SeatControlError || error instanceof RoomEntryError) ? error.status : error instanceof RuleError ? 409 : error instanceof SyntaxError ? 400 : 500, headers: noStore, }, ); } }