import { type ZodType } from 'zod'; import type { BasaltRoute, HttpReply, HttpRequest } from './route.js'; /** * Default cap on a {@link rawBody} body: 1 MiB. * * Deliberately small. Every payload this marker exists for — a Stripe, * Paddle, Lemon Squeezy, Dropbox, Microsoft Graph or GitHub webhook — is * measured in kilobytes, and the endpoint receiving it is unauthenticated by * construction (the signature IS the authentication, and it can only be * checked once the bytes are in memory). The cap is what stops such an * endpoint from being a memory amplifier. */ export declare const DEFAULT_RAW_BODY_MAX_BYTES: number; /** Limits for a {@link rawBody} route body. */ export interface RawBodyOptions { /** * Most bytes the body may carry. Over it: 413 `PAYLOAD_TOO_LARGE`, refused * on the declared `Content-Length` when there is one and on the bytes * actually received when there is not. Default {@link DEFAULT_RAW_BODY_MAX_BYTES}. */ maxBytes?: number; } /** What a `rawBody()` route's handler receives as `body`. */ export interface RawBody { /** * The exact bytes that arrived — never parsed, never re-serialised. This is * the message a provider signed; anything derived from a parsed object is a * different message. */ bytes: Buffer; /** * The declared `Content-Type` essence, lower-cased and without parameters * (`application/json`), or `undefined` when the client sent none. The * client's claim about the bytes, not a fact about them. */ contentType: string | undefined; /** The declared `Content-Length`, when the client sent one. */ contentLength: number | undefined; /** * `bytes` decoded as UTF-8 — the form most signature schemes are specified * against (`stripe.webhooks.constructEvent` takes a string). Decoding is * lossy for bytes that are not valid UTF-8; use {@link RawBody.bytes} when * the scheme is specified over octets. */ text(): string; } /** The resolved limits behind a {@link rawBody} marker. */ export interface ResolvedRawBodyOptions { maxBytes: number; } /** * Declares that a route wants the **untouched request bytes** — adapter-neutral: * the same route sees byte-identical input on Fastify, Express and Hono. * * ```ts * route({ * method: 'POST', url: '/webhooks/stripe', * body: rawBody({ maxBytes: 64 * 1024 }), * handler({ body, request }) { * const event = stripe.webhooks.constructEvent( * body.text(), * request.headers['stripe-signature'] as string, * secret, * ) * }, * }) * ``` * * Why this has to exist: every adapter parses `application/json` before a * handler runs, and a signature covers the bytes that arrived. `JSON.stringify` * of the parsed object is not an approximation of those bytes — key order, * whitespace, number formatting and escaping all differ — so a route that * verified against it would reject every genuine delivery (or, if it shrugged * the mismatch off, accept every forgery). * * The usual pipeline order is preserved: pre-hooks (rate limiting), enrichers * (tenant, user) and guards (auth, permissions) all run BEFORE a single body * byte is read, exactly as for {@link upload}. A body the route never gets to * read — a guard rejected first — is drained and the connection closed, so * nothing hangs. The bytes are never handed to a parser, this package's or the * app's. */ export declare function rawBody(options?: RawBodyOptions): ZodType; /** The resolved limits when `schema` came from {@link rawBody}; otherwise `undefined`. */ export declare function rawBodyOptionsOf(schema: unknown): ResolvedRawBodyOptions | undefined; /** True when a route's `body` is a {@link rawBody} declaration — adapters skip their own body parsing for it. */ export declare const isRawBody: (schema: unknown) => boolean; /** * A predicate telling whether an inbound `method` + path belongs to one of * these routes' {@link rawBody} declarations. * * Adapters that parse bodies in middleware — before a route is matched — need * this to step aside for exactly those paths (Express's `express.json({ type })`, * the Hono plugin's bounded-read middleware). Path parameters (`:id`) match one * segment; a `*` segment matches one segment and a trailing `/*` matches the * rest. Anything else is compared literally, so an unrecognised pattern * under-matches rather than over-matches — the adapter then falls back to its * own capture instead of silently leaving a JSON route unparsed. */ export declare function rawBodyRouteMatcher(routes: readonly BasaltRoute[]): (method: string, path: string) => boolean; /** * One request's raw body: reads it once, only when the pipeline asks (after * enrichers and guards), and releases whatever is left otherwise. Created by * the pipeline; not public API. */ export declare class RawBodySession { private readonly request; readonly options: ResolvedRawBodyOptions; private settled; constructor(request: HttpRequest, options: ResolvedRawBodyOptions); /** The handler's `body`. Throws 413 over the cap, 500 when no adapter supplied bytes. */ read(): Promise; /** * Called once the route is done. A body nobody read (a guard rejected first) * gets `Connection: close` and is drained up to the cap — the request can * never hang on an unread body. */ release(reply: HttpReply): void; private collect; /** Whether the request's own framing says it is sending bytes. */ private declaresBody; }