diff --git a/.dockerignore b/.dockerignore index 0b09575f7..33b4697ce 100644 --- a/.dockerignore +++ b/.dockerignore @@ -78,8 +78,8 @@ CONTRIBUTING.md CONTRIBUTING-AI.md CODE_OF_CONDUCT.md SECURITY.md -LICENSE.md -LICENSE +# LICENSE / LICENSE.md are deliberately NOT ignored: AGPL-3.0 conveys with +# the software, so the licence text ships inside the image. AGENTS.md .planning docs diff --git a/src/app/api/workouts/batch/route.ts b/src/app/api/workouts/batch/route.ts index 8982eb914..a490b2e3d 100644 --- a/src/app/api/workouts/batch/route.ts +++ b/src/app/api/workouts/batch/route.ts @@ -87,13 +87,13 @@ const PR_DETECTION_SILENT_THRESHOLD = 50; const BATCH_RATE_LIMIT_MAX = 60; const BATCH_RATE_LIMIT_WINDOW_MS = 60 * 1000; -// 5 MB request-body ceiling. The route geometry is the largest tail in -// a workout payload; with the 20 000-point cap on a single LineString -// AND the 100-workout cap per batch, the worst-case body is bounded -// well below this ceiling for typical lon/lat encodings. A request -// above this ceiling almost certainly indicates a misbehaving client -// or an attempted DoS — return 413 so the iOS client falls back to -// one-workout-per-call. +// 5 MB request-body ceiling (pre-existing). The route geometry is the +// largest tail in a workout payload; a typical batch sits comfortably +// under this, but the 20 000-point LineString cap combined with the +// 100-workout batch cap allows a theoretical tens-of-MB body for an +// extreme GPS-heavy backlog. Such a body returns 413 so the iOS client +// falls back to smaller / one-workout-per-call batches — which is also +// the correct response to a misbehaving client or an attempted DoS. const MAX_BODY_BYTES = 5 * 1024 * 1024; /** @@ -178,10 +178,10 @@ async function postBatch(request: NextRequest): Promise { // The Content-Length pre-flight above bounds the common case, but a // chunked upload carries no Content-Length. The `safeJson` body cap // backstops that gap: the raw text is measured before `JSON.parse`, - // so an over-limit chunked body returns a clean 413 rather than - // buffering an unbounded payload onto the heap. Reuses the same 5 MB - // ceiling — the route's 100-workout × 20 000-point worst case sits - // well below it, so a real iOS batch is never rejected. + // so an over-limit chunked body returns a clean 413 before the parse + // cost. Reuses the same 5 MB ceiling — a typical iOS batch sits under + // it; an extreme GPS-heavy backlog that exceeds it falls back to + // smaller batches client-side. const { data: rawBody, error: jsonError } = await safeJson(request, { maxBytes: MAX_BODY_BYTES, }); diff --git a/src/lib/api-response.ts b/src/lib/api-response.ts index ff8832485..3f5a17d5b 100644 --- a/src/lib/api-response.ts +++ b/src/lib/api-response.ts @@ -201,13 +201,16 @@ export function apiError( * Returns the parsed body or a 400 error response if parsing fails. * * `opts.maxBytes` opts a route into a hard body-size cap. The check - * runs against the raw text rather than the parsed object, so a - * gigabyte payload never reaches `JSON.parse` and never lands in - * heap. Single-record routes can pass a tight cap (a few KB); batch - * routes that legitimately accept large payloads can pass a larger - * one (a few hundred KB). When `maxBytes` is omitted the route - * inherits the Next.js runtime default — pre-existing behaviour, no - * regression risk for callers that haven't adopted the parameter. + * runs against the raw text length rather than the parsed object, so an + * over-limit payload returns 413 before reaching `JSON.parse` and never + * builds an object graph. (`request.text()` still buffers the raw body + * into a string, so the cap bounds the parse/object cost, not the + * initial read — keep caps comfortably above the largest legitimate + * payload.) Single-record routes can pass a tight cap; batch routes + * that legitimately accept large payloads pass a larger one. When + * `maxBytes` is omitted the route inherits the Next.js runtime default — + * pre-existing behaviour, no regression risk for callers that haven't + * adopted the parameter. */ export async function safeJson( request: Request, diff --git a/src/lib/jobs/reminder-worker.ts b/src/lib/jobs/reminder-worker.ts index 45a379751..69c6e3c3a 100644 --- a/src/lib/jobs/reminder-worker.ts +++ b/src/lib/jobs/reminder-worker.ts @@ -2824,7 +2824,7 @@ export async function startReminderWorker() { if (!DENSE_INTRADAY_RETENTION_ENABLED) { workerLog( "info", - "[dense-intraday-retention] disabled — skipping the nightly walk (P2002 fold collision; re-enable via DENSE_INTRADAY_RETENTION_ENABLED once reworked)", + "[dense-intraday-retention] disabled via DENSE_INTRADAY_RETENTION_ENABLED — skipping the nightly walk (operator kill-switch)", ); } else { const denseSummary = await runDenseIntradayRetention(