import type { Readable } from 'node:stream'; import type { output as ZodOutput, ZodType } from 'zod'; export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'; /** * Framework-neutral request seen by handlers, enrichers and guards. Adapters * (Fastify/Express/Hono) build this from their native request; `raw` is the * escape hatch to that native object. */ export interface HttpRequest { method: string; /** Full URL path (+ query string). */ url: string; /** Header names are lower-cased by every adapter. */ headers: Record; params: Record; query: unknown; body: unknown; ip?: string; /** Matched route template (e.g. `/users/:id`), when the adapter knows it. */ routePattern?: string; /** * The unread request body as a stream — a Node `Readable` or a web * `ReadableStream`. Adapters set it only for `upload()` and `rawBody()` * routes, whose body they deliberately leave unparsed; the pipeline streams * it through the multipart parser, or reads it to a capped buffer. Absent * everywhere else. */ bodyStream?: Readable | ReadableStream; /** * The untouched request bytes, when the adapter could NOT leave the body * unread and kept them instead — an app-wide `express.json()` is the case * this exists for, since its `verify` hook is the only place the original * bytes still are. Set only for `rawBody()` routes, and only when * `bodyStream` cannot be offered; the pipeline prefers it over reading a * stream that is already spent. Absent everywhere else. */ bodyBytes?: Uint8Array; raw: unknown; } /** * Framework-neutral response. `code().header().send()` is the common surface; * `sent` lets the adapter know whether to also emit the handler's return value. */ export interface HttpReply { code(status: number): this; header(name: string, value: string): this; /** Sends the payload; omit it for an empty body (e.g. a 204). */ send(payload?: unknown): unknown; readonly sent: boolean; readonly statusCode: number; raw: unknown; } type Infer = S extends ZodType ? ZodOutput : undefined; export interface HandlerArgs { body: Infer; query: Infer; params: Infer

; request: HttpRequest; reply: HttpReply; } /** * What a route declares about itself for other plugins to enforce. * * Open by design — the index signature keeps every existing `meta` compiling, * and applications legitimately put their own keys here. What it adds is the * shape of the keys the toolkit *does* know: `meta: { can: 123 }` is now an * error, and an editor can complete the names. * * Plugins declare their own keys by augmentation, the same pattern * `BasaltHooks` uses: * * ```ts * declare module '@basaltkit/http' { * interface RouteMeta { * can?: string | string[] * } * } * ``` * * What this does NOT catch is a misspelt key: `subcribed: 'pro'` still * compiles, because the index signature has to accept unknown names. That gap * is closed at boot instead — the adapters refuse to start on a guard key with * no plugin behind it, and `subscriptionsPlugin` refuses on a plan that is not * in the catalogue. */ export interface RouteMeta { [key: string]: unknown; } export interface BasaltRoute { method: HttpMethod; url: string; body?: ZodType | undefined; query?: ZodType | undefined; params?: ZodType | undefined; /** Per-status response schemas — feed OpenAPI/SDK (Metadata), not validated at runtime. */ response?: Record | undefined; /** Free-form metadata read by other plugins (auth, permissions, rate-limit…). */ meta?: Record | undefined; handler: (args: HandlerArgs) => unknown; } /** * Defines an end-to-end typed route once — it runs unchanged on every adapter * (Fastify, Express, Hono). Body/query/params types are INFERRED from the Zod * schemas; no manual generics. */ export declare function route(config: { method: HttpMethod; url: string; body?: B; query?: Q; params?: P; response?: Record; meta?: Record; handler: (args: HandlerArgs) => unknown; }): BasaltRoute; export {};