import { Readable } from 'node:stream'; import { type ZodType } from 'zod'; import type { HttpReply, HttpRequest } from './route.js'; /** Limits for a {@link upload} route body. `maxBytes` and `maxFiles` are required on purpose. */ export interface UploadOptions { /** Most bytes the whole request body may carry (multipart framing included). Over it: 413. */ maxBytes: number; /** Most file parts accepted. One more: 400 `TOO_MANY_FILES`. */ maxFiles: number; /** Most bytes a single file may carry. Default: `maxBytes`. Over it: 413. */ maxFileBytes?: number; /** Most non-file fields accepted. Default: 50. One more: 400 `TOO_MANY_FIELDS`. */ maxFields?: number; /** Most bytes one field value may carry. Default: 64 KiB. Over it: 413. */ maxFieldBytes?: number; /** Most bytes of one part's header block. Default: 8 KiB. Over it: 400. */ maxHeaderBytes?: number; /** * Accepted declared content types of FILE parts — exact (`image/png`) or a * wildcard subtype (`image/*`). Anything else: 415. Default: any. This is * the client's claim, not the bytes' — sniff the content before trusting it. */ allowedTypes?: readonly string[]; } /** One uploaded file, handed to the handler while its bytes are still arriving. */ export interface UploadedFile { /** The form field the file was sent under. */ field: string; /** Sanitised basename (no directories, controls or bidi tricks) — a label, never a storage key. */ filename: string; /** The part's declared `Content-Type` essence (default `application/octet-stream`). Client-controlled. */ declaredType: string; /** * The file's bytes, streamed. Consume it (or `destroy()` it) before asking * for the next file; a file left unread is skipped when the next one is * requested. It errors with the limit's HttpError if the upload breaks one. */ stream: Readable; /** * The size the client declared for THIS part (its own `Content-Length` * header), when it sent one. RFC 7578 does not require it and browsers never * send it, so it is usually `undefined` — there is no honest way to derive a * per-file size from the request's `Content-Length`, which covers every part * plus the multipart framing. Pass it straight to a backend that wants an * exact size (`files.upload(file.stream, { contentLength })`); the real size * is still measured from the bytes that arrive. */ declaredLength?: number; } /** What an `upload()` route's handler receives as `body`. */ export interface UploadBody { /** * The file parts, in order, as they arrive — `for await (const file of body.files)`. * Nothing is read from the network until this is iterated. */ files: AsyncIterable; /** * Non-file fields (null-prototype object; a repeated name keeps its last * value). Filled as the body is read: a field sent before a file is present * when that file is yielded, and every field once `files` is exhausted. */ fields: Record; /** * The request's declared `Content-Length`, when the client sent one (a * chunked upload carries none). It bounds the WHOLE body — every part plus * the multipart framing — so it is an upper bound for a single file, never * its size. For that, use `file.declaredLength`. */ contentLength?: number; } export interface ResolvedUploadOptions { maxBytes: number; maxFiles: number; maxFileBytes: number; maxFields: number; maxFieldBytes: number; maxHeaderBytes: number; allowedTypes: readonly string[] | undefined; } /** * Declares a streaming `multipart/form-data` body for a route — adapter-neutral: * the same route accepts uploads on Fastify, Express and Hono. * * ```ts * route({ * method: 'POST', url: '/documents', * body: upload({ maxBytes: 20 * 1024 * 1024, maxFiles: 1, allowedTypes: ['application/pdf'] }), * meta: { auth: true }, * async handler({ body }) { * for await (const file of body.files) await files().upload({ ..., body: file.stream }) * }, * }) * ``` * * The whole pipeline — pre-hooks (rate limit), enrichers (tenant, user) and * guards (auth, permissions) — runs BEFORE a single body byte is read. The body * is then parsed as the handler consumes it, never buffered, with every limit * enforced on the bytes actually received (a declared `Content-Length` over * `maxBytes` is refused up front). An upload the handler leaves unread is * drained (up to `maxBytes`) and the connection closed, so nothing hangs. */ export declare function upload(options: UploadOptions): ZodType; /** The resolved limits when `schema` came from {@link upload}; otherwise `undefined`. */ export declare function uploadOptionsOf(schema: unknown): ResolvedUploadOptions | undefined; /** True when a route's `body` is an {@link upload} declaration — adapters skip their own body parsing for it. */ export declare const isUploadBody: (schema: unknown) => boolean; /** * One request's upload: validates the request framing, then pulls bytes from * the transport only as the handler consumes files (or when the route is done * and the rest must be drained). Created by the pipeline; not public API. */ export declare class UploadSession { private readonly request; readonly options: ResolvedUploadOptions; private readonly fields; private source; private parser; private received; private fileCount; private fieldCount; private current; private readonly queue; private lastYielded; private finished; private failure; private waiter; private running; private demand; private discarding; constructor(request: HttpRequest, options: ResolvedUploadOptions); /** Validates the request framing (415/400/413) and returns the handler's `body`. */ open(): UploadBody; /** * Called once the route is done (handler returned or threw, or a guard * rejected). An upload not read to the end gets `Connection: close` and is * drained in the background up to `maxBytes`, then left for the server to * close — the request can never hang on an unread body. */ release(reply: HttpReply): void; private declaredLength; private next; /** The previous file, if nobody is reading it, is skipped so the parser can move on. */ private abandonLast; private wantMore; private kick; private pump; private wake; private fail; private partStart; private partData; private partEnd; }