Files
Dune/app/api/rooms/[code]/control/route.ts
T

198 lines
6.4 KiB
TypeScript

import { requestOrigin } from '@/lib/request-origin';
import {
authenticate,
createSeatHandover,
revokeSeatHandover,
claimSeatHandover,
recoverSeat,
SeatControlError,
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<string, unknown>;
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.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 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.code
: error instanceof RuleError
? 'INVALID_SESSION'
: error instanceof SyntaxError
? 'INVALID_CONTROL_REQUEST'
: 'SERVER_ERROR',
},
{
status:
error instanceof SeatControlError
? error.status
: error instanceof RuleError
? 409
: error instanceof SyntaxError
? 400
: 500,
headers: noStore,
},
);
}
}