fix: enforce API request limits
Add a single platform-owned request-body policy (hono/body-limit) with small, named limits for Better Auth and Core's grant-mutation endpoint, and a new required maxBodyBytes field on the executable module route contract so every module route makes a conscious no-body-or-bounded-body choice, capped at a platform ceiling. Every oversized request gets one sanitized 413 contract and never reaches business logic, authorization, or a handler. Also configure finite Node header/request/keep-alive timeouts, including the non-obvious connectionsCheckingInterval needed for them to take effect promptly, so a slow-header or slow-body connection cannot hold a socket indefinitely. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
cafd513019
commit
d3c6bd73d3
@@ -0,0 +1,48 @@
|
||||
import { MODULE_ROUTE_MAX_BODY_BYTES_CEILING } from "@ekklesia/contracts";
|
||||
import type { Context } from "hono";
|
||||
import { bodyLimit } from "hono/body-limit";
|
||||
|
||||
/**
|
||||
* The platform's one request-body policy, centralized here rather than scattered as magic numbers
|
||||
* across every route file. Every limit below is deliberately small relative to the actual payload
|
||||
* shapes it guards:
|
||||
*
|
||||
* - `AUTH_REQUEST_MAX_BODY_BYTES` (8 KiB): every enabled Better Auth route
|
||||
* (`/api/auth/sign-up/email`, `/sign-in/email`, `/sign-out`, ...) accepts only an email, a
|
||||
* password (bounded to 128 characters — `core/src/identity/runtime.ts`), and an optional short
|
||||
* display name; the pinned release adds no other enabled provider or field (ADR 0011). A real
|
||||
* payload is well under 200 bytes; 8 KiB leaves generous headroom for a verbose email address or
|
||||
* name while remaining two orders of magnitude below the module ceiling.
|
||||
* - `CORE_MUTATION_MAX_BODY_BYTES` (16 KiB): the one Core JSON mutation body, explicit-grant
|
||||
* creation (`POST /api/v1/grants`), accepts `subjectUserId` (<=128 chars), `permission` (a bounded
|
||||
* identifier), an optional `scope` (<=256 chars), and an optional ISO `expiresAt` string — every
|
||||
* field individually bounded well under 1 KiB combined; 16 KiB leaves headroom without inviting
|
||||
* abuse.
|
||||
* - `MODULE_ROUTE_MAX_BODY_BYTES_CEILING` (1 MiB, re-exported from `@ekklesia/contracts` — the same
|
||||
* constant a module's own route declaration is validated against at registration) is the hard
|
||||
* ceiling no module route may exceed and doubles as the platform's overall request-body ceiling:
|
||||
* nothing in this platform ever declares, or enforces, a larger single request body than this.
|
||||
* Large media transfer is explicitly out of scope for ordinary module HTTP bodies — it belongs to
|
||||
* a future dedicated storage design, not to widening this ceiling.
|
||||
*/
|
||||
export const AUTH_REQUEST_MAX_BODY_BYTES = 8 * 1024;
|
||||
export const CORE_MUTATION_MAX_BODY_BYTES = 16 * 1024;
|
||||
export { MODULE_ROUTE_MAX_BODY_BYTES_CEILING };
|
||||
|
||||
/** The one stable, sanitized public contract for every request-body-size rejection across the
|
||||
* platform — auth, Core, and module routes alike. Never leaks the configured limit, the offending
|
||||
* size, a declared Content-Length, or any body content. */
|
||||
export function requestBodyTooLargeResponse(c: Context): Response {
|
||||
c.header("Cache-Control", "no-store");
|
||||
return c.json({ code: "REQUEST_BODY_TOO_LARGE" }, 413);
|
||||
}
|
||||
|
||||
/** A single shared `hono/body-limit` factory so every call site uses the identical sanitized 413
|
||||
* contract rather than redefining `onError` per route. Covers both a declared oversized
|
||||
* `Content-Length` and an oversized chunked/streamed body with no declared length — see
|
||||
* `hono/body-limit`'s pinned implementation, which checks `Content-Length` synchronously before
|
||||
* ever reading the stream, and otherwise reads and counts the stream incrementally, rejecting as
|
||||
* soon as the running total exceeds `maxSize` rather than buffering the whole body first. */
|
||||
export function platformBodyLimit(maxSize: number) {
|
||||
return bodyLimit({ maxSize, onError: requestBodyTooLargeResponse });
|
||||
}
|
||||
Reference in New Issue
Block a user