/** * Copyright (c) 2026 AzerothJS. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /** * Request body readers with limits ON by default. * * Express ships body parsing as an external middleware with no limit unless configured; * unbounded bodies are a one-request memory DoS. Here reading a body is an explicit, typed, * LIMITED operation: every reader enforces a byte cap while STREAMING, so neither a lying * Content-Length nor a chunked body without one can exceed it - the read aborts the moment * the cap is crossed, not after buffering. * * Readers throw the kernel's own errors (PayloadTooLargeError, UnsupportedMediaTypeError, * BadRequestError), which the one error path maps to correct statuses - a handler simply * awaits `readJson(request)` and malformed input becomes a 400/413/415 with a stable code. * * Multipart lives in its own module (multipart.ts): a from-scratch stream parser deserves * focused code and fixtures, not a corner of this file. */ import type { Issue } from '@azerothjs/schema'; /** * OPTIONAL adapter capabilities. The kernel is 100% web-standard: these symbols name fast * lanes an adapter's Request MAY implement (the Node adapter does), and every consumer here * falls back to the spec surface when they are absent - a plain undici Request, a test * Request, or another runtime's Request all work identically, just without the shortcut. */ /** Capability key: case-insensitive single-header reads without building a Headers object. */ export declare const fastHeaderLookup: unique symbol; /** Capability key: the already-buffered request body, skipping a re-read of the stream. */ export declare const fastRawBody: unique symbol; /** Capability key: the peer address, which the web-standard Request does not expose. */ export declare const socketAddress: unique symbol; /** * Releases the request's client-disconnect watch. The adapter calls it when the RESPONSE * closes, which is the only event that bounds both the request and a still-streaming body. */ export declare const releaseSignal: unique symbol; /** The shapes behind the capability symbols. */ export interface FastCapabilities { /** Case-insensitive single-header read without constructing a Headers object. */ [fastHeaderLookup]?(name: string): string | null; /** Reads the whole body with the limit enforced while streaming; rejects PayloadTooLarge. */ [fastRawBody]?(limit: number): Promise; /** The peer's remote IP as the socket sees it (before any proxy header), or null off-socket. */ [socketAddress]?(): string | null; /** * @see releaseSignal * * Optional on the capability bag, because a Request from another runtime has no such * method - the node adapter's own request always does, and its return type says so. */ [releaseSignal]?(): void; } /** The default body cap: 1 MiB, matching the conservative end of ecosystem defaults. */ export declare const DEFAULT_BODY_LIMIT: number; /** * Limits for the body readers ({@link readRaw}/{@link readText}/{@link readJson}/ * {@link readForm}/{@link readValidated}). The cap is ON by default - an absent option means * {@link DEFAULT_BODY_LIMIT}, never unlimited. */ export interface ReadOptions { /** Maximum body size in bytes (default {@link DEFAULT_BODY_LIMIT}). */ limit?: number; } /** * Reads the raw body into one Uint8Array, enforcing the limit while streaming. The shared * primitive under every other reader. A declared Content-Length above the limit fails fast * WITHOUT reading; an undeclared or lying length is caught by the running count. * * A body can only be read ONCE (the web standard's rule, and the reason a raw-body HMAC check * must hand its bytes on rather than let the handler read again). A second read throws * TypeError - the same shape the standard reader produces on a disturbed stream - because the * alternative on a drained socket is a promise that never settles and a request that never ends. */ export declare function readRaw(request: Request, options?: ReadOptions): Promise; /** Reads the body as UTF-8 text (with the same streaming limit). */ export declare function readText(request: Request, options?: ReadOptions): Promise; /** * Reads and parses a JSON body. Enforces `Content-Type: application/json` (including * `+json` suffixes like application/problem+json) unless `accept` widens it - silently * parsing text/plain as JSON is how CSRF-adjacent content-type confusion starts. */ export declare function readJson(request: Request, options?: ReadOptions & { accept?: (contentType: string) => boolean; }): Promise; /** * Reads an application/x-www-form-urlencoded body (what a plain HTML form posts) into * URLSearchParams - the standard container, preserving repeated keys. */ export declare function readForm(request: Request, options?: ReadOptions): Promise; /** * The STRUCTURAL shape of a validator this module accepts - `@azerothjs/schema`'s Schema * satisfies it, and so does anything else with a compatible safeParse. Structural on purpose: * the boundary accepts whatever schema library the app uses; the ISSUE shape is the one * declaration the whole framework speaks (`@azerothjs/schema`'s Issue, imported type-only). */ export interface SchemaLike { safeParse(value: unknown, options?: { mode?: 'all' | 'first'; }): { ok: true; value: T; } | { ok: false; errors: Record; issues?: Issue[]; }; } /** The structural `~standard` half: any Standard Schema v1 validator (Zod, Valibot, ArkType). */ export interface StandardSchemaLike { '~standard': { validate(value: unknown): { value: T; issues?: undefined; } | { issues: ReadonlyArray<{ message: string; path?: ReadonlyArray; }>; } | Promise<{ value: T; issues?: undefined; } | { issues: ReadonlyArray<{ message: string; path?: ReadonlyArray; }>; }>; }; } /** A validation success: the parsed (normalized) value. */ export interface ParseOk { ok: true; value: unknown; } /** A validation failure: the flat field-path map plus the ordered issue list. */ export interface ParseErr { ok: false; errors: Record; issues?: Issue[]; } /** * ONE schema unification for every boundary: a schema's capabilities are sniffed, never * type-dispatched. A native `@azerothjs/schema` value keeps its one-pass `safeParse` (issue * codes included); any other Standard Schema validator runs `~standard.validate`, its issues * mapped to the flat field-path errors the whole framework speaks. Failures are returned, * never thrown - each caller raises its own dialect. Consumed by {@link readValidated} and * the api layer's `register`; module-exported, deliberately not on the package index. */ export declare function parseAny(schema: unknown, value: unknown, mode?: 'all' | 'first'): Promise; /** * Reads and validates a JSON body in one call: `readJson` (Content-Type + limits enforced) * then the schema - a native safeParse or any Standard Schema validator, unified by * {@link parseAny}. A failure throws {@link ValidationError} - the 422 whose * `details.fields` the frontend form's setError consumes and whose `details.issues` carry * the stable codes. The happy path returns the schema's parsed (normalized) value, typed. */ export declare function readValidated(request: Request, schema: SchemaLike | StandardSchemaLike, options?: ReadOptions & { mode?: 'all' | 'first'; }): Promise; /** The media type of the request, lowercased, without parameters ("text/html;q=1" -> "text/html"). */ export declare function mediaTypeOf(request: Request): string;