diff --git a/apps/planner/app/lib/sessions.ts b/apps/planner/app/lib/sessions.ts index 9b4bb33..735840f 100644 --- a/apps/planner/app/lib/sessions.ts +++ b/apps/planner/app/lib/sessions.ts @@ -106,12 +106,19 @@ export function initializeSessionWithNoGoAreas( }); } -export async function listSessions(): Promise { +// Default page size for the listing API. Hard cap so an unbounded +// caller can't request thousands of rows (or trigger a full scan). +const DEFAULT_LIST_LIMIT = 50; +const MAX_LIST_LIMIT = 200; + +export async function listSessions(limit: number = DEFAULT_LIST_LIMIT): Promise { + const bounded = Math.min(Math.max(1, limit), MAX_LIST_LIMIT); return getDb() .select() .from(sessions) .where(eq(sessions.closed, false)) - .orderBy(desc(sessions.lastActivity)); + .orderBy(desc(sessions.lastActivity)) + .limit(bounded); } export async function expireSessions(maxAgeDays: number = 7): Promise { diff --git a/apps/planner/app/routes/api.sessions.ts b/apps/planner/app/routes/api.sessions.ts index 3aee552..5fbd2d4 100644 --- a/apps/planner/app/routes/api.sessions.ts +++ b/apps/planner/app/routes/api.sessions.ts @@ -42,9 +42,14 @@ export async function action({ request }: Route.ActionArgs) { }); } -export async function loader(_args: Route.LoaderArgs) { +export async function loader({ request }: Route.LoaderArgs) { return withDb(async () => { - const sessions = await listSessions(); + const url = new URL(request.url); + // Accept an explicit `?limit=` but rely on listSessions to clamp + // it to a sane upper bound. + const limitParam = Number(url.searchParams.get("limit")); + const limit = Number.isFinite(limitParam) && limitParam > 0 ? limitParam : undefined; + const sessions = await listSessions(limit); return data({ sessions }); }); }