Initial commit

This commit is contained in:
Quintine committed 2026-09-08 07:37:52 +10:00
commit 8c709fb52f
771 files changed
+222983

No files matched your search

+110
View File
@@ -0,0 +1,110 @@
import {
authenticate,
recoverSeat,
SeatControlError,
setRecoveryKey,
} from '@/db/rooms';
import { RuleError } from '@/game/engine';
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') !== url.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>;
if (input.type === 'setRecoveryKey') {
const token =
req.headers
.get('cookie')
?.split(';')
.map((part) => part.trim())
.find((part) => part.startsWith(`dune_${code}=`))
?.split('=')[1] ?? '';
const auth = await authenticate(code, token);
return Response.json(
await setRecoveryKey(code, auth, input.version, input.recoverySecret),
{ headers: noStore },
);
}
if (input.type !== 'recoverSeat')
throw new SeatControlError(
'Choose a recovery operation.',
'INVALID_CONTROL_REQUEST',
400,
);
const result = await recoverSeat(code, {
playerId: input.playerId,
recoverySecret: input.recoverySecret,
operationId: input.operationId,
newSessionToken: input.newSessionToken,
});
return Response.json(
{
view: result.view,
recovered: result.recovered,
replayed: result.replayed,
},
{
headers: {
...noStore,
'Set-Cookie': `dune_${code}=${result.token}; HttpOnly; SameSite=Strict; Path=/api/rooms/${code}; Max-Age=2592000${url.protocol === 'https:' ? '; Secure' : ''}`,
},
},
);
} catch (error) {
return Response.json(
{
error:
error instanceof RuleError
? error.message
: error instanceof SyntaxError
? 'Invalid JSON.'
: 'The server could not complete seat recovery.',
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,
},
);
}
}
+158
View File
@@ -0,0 +1,158 @@
import {
readSeatView,
authenticate,
joinRoom,
act,
RoomEntryError,
continueRoomBots,
continueRoomAutomatic,
needsAutomaticRoomRecovery,
type SeatAuth,
} from '@/db/rooms';
import { RuleError } from '@/game/engine';
import type { FactionId } from '@/game/catalog';
import { waitUntil } from 'cloudflare:workers';
// Coalesce overlapping polls inside this isolate. Database CAS still fences
// other isolates, restarts, seat recovery, and human actions.
const continuations = new Map<string, Promise<void>>();
function resumeRoom(code: string): Promise<void> {
const existing = continuations.get(code);
if (existing) return existing;
const pending = (async () => {
await continueRoomAutomatic(code);
await continueRoomBots(code);
})().finally(() => {
if (continuations.get(code) === pending) continuations.delete(code);
});
continuations.set(code, pending);
return pending;
}
const codeOf = (req: Request) => {
const code = new URL(req.url).pathname.split('/').pop()!.toUpperCase();
if (!/^[A-Z2-9]{8}$/.test(code))
throw new RuleError('Enter the eight-character room code.');
return code;
};
const tokenOf = (req: Request, code: string) =>
req.headers
.get('cookie')
?.split(';')
.map((x) => x.trim())
.find((x) => x.startsWith(`dune_${code}=`))
?.split('=')[1] ?? '';
const failure = (e: unknown) =>
Response.json(
{
...(e instanceof RoomEntryError ? { code: e.code } : {}),
error:
e instanceof RuleError
? e.message
: e instanceof SyntaxError
? 'Invalid JSON.'
: 'The game server could not complete this request.',
},
{
status:
e instanceof RoomEntryError
? e.status
: e instanceof RuleError
? 409
: e instanceof SyntaxError
? 400
: 500,
headers: { 'Cache-Control': 'no-store' },
},
);
export async function GET(req: Request) {
try {
const code = codeOf(req),
id = await authenticate(code, tokenOf(req, code));
const view = await readSeatView(code, id);
// Reconnect resumes persisted automatic effects/acknowledgements and AI work
// after an interruption. Actual human choices remain engine-owned decisions.
if (view.botsPending || needsAutomaticRoomRecovery(view))
waitUntil(resumeRoom(code));
return Response.json(view, {
headers: { 'Cache-Control': 'no-store' },
});
} catch (e) {
return failure(e);
}
}
export async function POST(req: Request) {
try {
if (
req.headers.get('origin') &&
req.headers.get('origin') !== new URL(req.url).origin
)
return Response.json(
{ error: 'Invalid request origin.' },
{ status: 403 },
);
const code = codeOf(req);
const raw = await req.text();
if (raw.length > 8000)
return Response.json({ error: 'Request too large.' }, { status: 413 });
const body = JSON.parse(raw);
if (!body || typeof body !== 'object' || Array.isArray(body))
throw new RuleError('The request is invalid.');
if (body.type === 'join') {
const existing = tokenOf(req, code);
let auth: SeatAuth | null = null;
if (existing) {
try {
auth = await authenticate(code, existing);
} catch (error) {
if (!(error instanceof RuleError)) throw error;
}
}
if (body.entry === undefined && auth) {
return Response.json(await readSeatView(code, auth), {
headers: { 'Cache-Control': 'no-store' },
});
}
if (typeof body.name !== 'string' || typeof body.faction !== 'string')
throw new RuleError('Enter your name and faction.');
const r = await joinRoom(
code,
body.name,
body.faction as FactionId,
body.entry,
auth,
);
return Response.json(
{
...r.view,
...(r.entryReceipt ? { entryReceipt: r.entryReceipt } : {}),
...(r.alreadySeated ? { alreadySeated: true } : {}),
},
{
headers: {
...(r.token
? {
'Set-Cookie': `dune_${code}=${r.token}; HttpOnly; SameSite=Strict; Path=/api/rooms/${code}; Max-Age=2592000${new URL(req.url).protocol === 'https:' ? '; Secure' : ''}`,
}
: {}),
'Cache-Control': 'no-store',
},
},
);
}
const id = await authenticate(code, tokenOf(req, code));
if (
!Number.isSafeInteger(body.version) ||
!body.action ||
typeof body.action !== 'object'
)
throw new RuleError('The action is invalid.');
const view = await act(code, id, body.version, body.action);
if (view.botsPending || needsAutomaticRoomRecovery(view))
waitUntil(resumeRoom(code));
return Response.json(view, {
headers: { 'Cache-Control': 'no-store' },
});
} catch (e) {
return failure(e);
}
}
+70
View File
@@ -0,0 +1,70 @@
import { createRoom, RoomEntryError } from '@/db/rooms';
import { RuleError } from '@/game/engine';
import type { FactionId } from '@/game/catalog';
export async function POST(req: Request) {
try {
if (
req.headers.get('origin') &&
req.headers.get('origin') !== new URL(req.url).origin
)
return Response.json(
{ error: 'Invalid request origin.' },
{ status: 403 },
);
const raw = await req.text();
if (raw.length > 4000)
return Response.json({ error: 'Request too large.' }, { status: 413 });
const body = JSON.parse(raw);
if (
!body ||
typeof body !== 'object' ||
Array.isArray(body) ||
typeof body.name !== 'string' ||
typeof body.faction !== 'string' ||
!Array.isArray(body.expansions) ||
typeof body.advanced !== 'boolean'
)
throw new RuleError('Enter a name, faction and rules selection.');
const result = await createRoom(
body.name,
body.faction as FactionId,
body.advanced,
body.expansions,
body.entry,
);
return Response.json(
{
...result.view,
...(result.entryReceipt ? { entryReceipt: result.entryReceipt } : {}),
},
{
status: 201,
headers: {
'Set-Cookie': `dune_${result.view.code}=${result.token}; HttpOnly; SameSite=Strict; Path=/api/rooms/${result.view.code}; Max-Age=2592000${new URL(req.url).protocol === 'https:' ? '; Secure' : ''}`,
'Cache-Control': 'no-store',
},
},
);
} catch (e) {
return Response.json(
{
...(e instanceof RoomEntryError ? { code: e.code } : {}),
error:
e instanceof RuleError
? e.message
: e instanceof SyntaxError
? 'Invalid JSON.'
: 'Unable to create the room. Please retry.',
},
{
status:
e instanceof RoomEntryError
? e.status
: e instanceof RuleError || e instanceof SyntaxError
? 400
: 500,
headers: { 'Cache-Control': 'no-store' },
},
);
}
}