fix(planner): bound /api/sessions listing (default 50, max 200)

Addresses planner-audit #8. `listSessions()` had no LIMIT — every call
to `GET /api/sessions` returned every non-closed session and did a
full table scan ordered by last_activity. On a long-running planner
host that's both a memory cliff and a query-plan footgun.

Now: defaults to 50 rows, clamps to [1, 200], accepts `?limit=` for
pagination-light scenarios. `sessions_last_activity_idx` (added in
#438) backs the ORDER BY + LIMIT efficiently.
This commit is contained in:
Ullrich Schäfer 2026-05-26 00:46:38 +02:00
parent 11edcbccb3
commit 6f18ce8099
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
2 changed files with 16 additions and 4 deletions

View file

@ -106,12 +106,19 @@ export function initializeSessionWithNoGoAreas(
});
}
export async function listSessions(): Promise<SessionMetadata[]> {
// 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<SessionMetadata[]> {
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<number> {

View file

@ -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 });
});
}