chore: correct body-cap comments, dockerignore licence posture, stale log copy

- workouts/batch + safeJson: the body cap is a DoS ceiling, not a bound
  the worst-case payload always fits under; an extreme GPS-heavy backlog
  can exceed it and falls back to smaller batches client-side. Note that
  request.text() still buffers the raw body, so the cap bounds the parse
  cost, not the initial read.
- .dockerignore: keep LICENSE / LICENSE.md in the image — AGPL-3.0
  conveys with the software.
- reminder-worker: the dense walk flag is an operator kill-switch now
  that the fold is reworked, not a pending-rework gate.
This commit is contained in:
Marc-André Bombeck committed 2026-06-03 09:10:46 +02:00
1 parent 33358c7960
commit 8456d3bbc3
4 files changed
+24 -21

No files matched your search

+2 -2
View File
@@ -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
+11 -11
View File
@@ -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<Response> {
// 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<unknown>(request, {
maxBytes: MAX_BODY_BYTES,
});
+10 -7
View File
@@ -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<T = unknown>(
request: Request,
+1 -1
View File
@@ -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(