import { type RequestBudget } from "../budget.js"; import type { EffectLifecycleObserver } from "../effect-lifecycle.js"; import type { Version } from "../index.js"; import { type AroundCapabilityOptions, type CapabilityInterceptor } from "../internal/capability-runtime.js"; import type { ResponseObserverPlugin } from "../response-observer.js"; import { type Method } from "../router/router.js"; import type { InferOutput, StandardSchemaV1 } from "../schema/standard.js"; import { type RawBodyReaders } from "./body.js"; import type { Context, Platform, ResponseControls, RouteSchema } from "./context.js"; import { type UrlParts } from "./http.js"; import { type NodeServeOutcome } from "./node-outcome.js"; import type { NodeOutcomeRuntime, NodeRequestContext, NodeRequestHook, NodeResponseContext, NodeResponseHook, ResponseBodyHook, ResponseBodyReplacement, ResponseHeadersHook, ResponseHeadersView } from "./node-outcome-hook.js"; import { type QueryValue, queryObjectOf, searchOf } from "./query.js"; import type { ResponseContractRuntime } from "./response-contract-lane.js"; import type { ResponseObserverMethods, ResponseObserverRuntime } from "./response-observer-runtime.js"; import { CONTEXT_SEARCH, CONTEXT_SET, type HandlerResult, type ResponseResult, type StatusResponse } from "./runtime-core.js"; export type { NodeRequestContext, NodeRequestHook, NodeResponseContext, NodeResponseHook, NodeServeOutcome, ResponseBodyHook, ResponseBodyReplacement, ResponseHeadersHook, ResponseHeadersView, }; import type { IdempotencyRuntime } from "./idempotency-lane.js"; import { INSTALL_EFFECT_LEDGER, INSTALL_IDEMPOTENCY, INSTALL_MCP, INSTALL_NODE_DIRECT, INSTALL_RESPONSE_CONTRACT, INSTALL_RESPONSE_OBSERVER, INSTALL_SSE, INSTALL_WS, RESOLVE_NODE_MOUNT } from "./install.js"; import type { EffectLedgerRuntime } from "./ledger-lane.js"; import type { McpRuntime } from "./mcp-hook.js"; import type { ContextPlugin, IdentityPlugin, PluginTypeCollapsed, ServerTypeUnpinned } from "./plugin.js"; import type { AddRoute, EmptyRegistry, OutputOf, Registry, RouteInfoFor, WsRouteInfoFor } from "./registry.js"; import type { AdmissionController, AdmissionDecision, FetchHandler, McpPromptDescriptor, McpResourceDescriptor, Middleware, MountFetchOptions, PromptArgument, PromptMessage, ResponseFinalization, RouteDescriptor, RunningServer, ServerOptions, StopHook, ToolAnnotations } from "./server-types.js"; import type { SSEInit, TypedSSEStream } from "./sse.js"; import type { SseRuntime } from "./sse-hook.js"; import type { WebSocketHandler, WebSocketUpgradeOutcome } from "./websocket.js"; import type { WsRuntime } from "./ws-hook.js"; export type MaybePromise = T | Promise; /** * Internal request view. A real Web `Request` already satisfies this shape, so Web/edge runtimes pass * their `Request` **directly** (zero wrapper allocation on the hot path - `request` is simply absent and * {@link requestOf} returns the source itself). Node's adapter passes a *lazy* source whose `request` * getter builds an undici `Request` only when user code reads `c.req`, an onRequest/onResponse hook * needs it, or a body helper consumes it - so the common Node request never pays for a `Request` build. */ export interface RequestSource { readonly method: string; readonly url: string; readonly headers: Headers; header?(name: string): string | null; /** Pre-split pathname/search, when the source already had the origin-form target (the Node lazy * sources do) - saves synthesizing an absolute URL only to scan it apart again. */ readonly urlParts?: UrlParts; readonly body: ReadableStream | null; arrayBuffer(): Promise; json(): Promise; /** Optional runtime-native exact-byte reader (Bun's Request.bytes, when available). */ bytes?(): Promise; /** Present only when materializing a `Request` is non-trivial (the Node lazy source); for a real * `Request` passed as the source it's absent and {@link requestOf} returns the source itself. */ readonly request?: Request; /** Optional pre-`Request` reader surface for the transport cap. A source that reads its own * transport bytes (the Node lazy source reads the socket) exposes them here so a capped direct * read (`c.req.json()`) buffers off the transport instead of routing through the deferred * `Request`. `body` must stay the live stream - the cap still guards a chunked body mid-flight. */ rawBodyReaders?(): RawBodyReaders; } /** The empty context extension. `NonNullable` is `{}` without tripping noBannedTypes. */ type EmptyContext = NonNullable; /** * Extracts the app's platform `Env` from its context `Ctx`. `server()` seeds `Ctx` with * `{ env: Env }`, so this pulls that back out to type `fetch`/`toFetchHandler`'s `env` argument * against the app's declared bindings. Defaults to `unknown` when no env was declared. */ type EnvOf = Ctx extends { readonly env: infer E; } ? E : unknown; /** Internal, path-erased runtime context. The typed `Context` is a structural view of this. */ export interface RawContext { readonly req: Request; readonly request: Request; readonly json: (body: unknown, init?: ResponseInit | number) => Response; readonly text: (body: string, init?: ResponseInit | number) => Response; params: Record; headers: Record; query: unknown; readonly cookies: Readonly>; body: unknown; readonly set: ResponseControls; readonly [CONTEXT_SET]: () => CtxSet | undefined; readonly [CONTEXT_SEARCH]: string; readonly signal: AbortSignal; readonly budget: RequestBudget; readonly env: unknown; readonly clientIp: string | undefined; readonly waitUntil: (promise: Promise) => void; readonly boundedBody: (maxBytes?: number) => Promise; readonly boundedJson: (maxBytes?: number) => Promise; } export type OnRequestResult = Response | Request | undefined; /** Structural native mount contract. The serving adapter owns the concrete Node request/response * types; the kernel only selects a handler after proving that taking this lane cannot skip its * global lifecycle. `false` means the capability was unavailable at runtime, so the adapter must * retry the ordinary Web mount with the same untouched source. */ type NativeMountHandler = (request: unknown, response: unknown, platform?: Platform) => MaybePromise; interface NativeMountSelection { readonly handler: NativeMountHandler; readonly path: string; readonly stripPrefix: boolean; } /** The handler's permitted return type. When the route declares a `response` schema, the return is * constrained to the contract's type (a raw `Response`, or a `status(...)` early exit - both are * control flow, not the declared payload) - so the implementation can't drift from the declared * contract. Without a `response` schema it's unconstrained (`HandlerResult`), exactly as before. */ type ResponseOf = S extends { response: infer R extends StandardSchemaV1; } ? InferOutput | Response | ResponseResult : HandlerResult; /** A typed status result is control flow, not a context extension. */ type StatusResponseOf = T extends StatusResponse ? T : never; type ContextExtensionPart = T extends object ? T : EmptyContext; type ContextExtensionOf = [Exclude, Response | ResponseResult>] extends [never] ? EmptyContext : ContextExtensionPart, Response | ResponseResult>>; /** Keep concrete lifecycle return values for route inference, but don't let an untyped hook widen every route to unknown. */ type HookOutputOf = unknown extends Awaited ? never : Exclude, void>; type ReturnOfHookField = M extends { readonly [P in K]?: infer F; } ? F extends (...args: never[]) => infer R ? HookOutputOf : never : never; type MiddlewareOutputOf = ReturnOfHookField | ReturnOfHookField | ReturnOfHookField | ReturnOfHookField; /** * Public handler shape: context typed from the path, the (optional) schema, and * any accumulated middleware context `Ctx` (from `derive`/`decorate`). */ export type Handler = (ctx: Context & Ctx) => MaybePromise>; export type { AdmissionController, AdmissionDecision, FetchHandler, McpPromptDescriptor, McpResourceDescriptor, Middleware, MountFetchOptions, PromptArgument, PromptMessage, ResponseFinalization, RouteDescriptor, RunningServer, ServerOptions, StopHook, ToolAnnotations, }; export type AnyServer = Server; export { type ContextPlugin, type DefinePluginResult, defineContextPlugin, defineIdentityPlugin, definePlugin, defineRouterPlugin, type NifraPlugin, type PluginTypeCollapsed, } from "./plugin.js"; export type { IdentityPlugin }; export { pathnameOf, urlPartsOf } from "./http.js"; /** The 422 as plain data - the shape every lane's response wrapper takes, so a rejected body costs * what an accepted one costs: on Node it is written straight to the socket with a `content-length` * instead of being built as a `Response` and drained back out. */ export { plainValidationError } from "./validation.js"; export { type QueryValue, queryObjectOf, searchOf }; /** `ctx.set` carrying the lazy backings (`_headers`, `_cookies`) so `toResponse` can skip allocating * anything when no handler touched `c.set.*`. Server-internal. */ export type CtxSet = ResponseControls & { _headers?: Record; /** Accumulated `Set-Cookie` values - a list, since a `Record` would collapse multiple cookies. */ _cookies?: string[]; }; /** `major.minor` of a full version literal - patch releases never change the typed surface. */ type FeatureVersionOf = V extends `${infer Major}.${infer Minor}.${string}` ? `${Major}.${Minor}` : V; /** * The feature version of the `@nifrajs/core` copy that declared these types, derived from the single * `VERSION` literal (type-only import - erased, so no runtime cycle with the package root). */ export type NifraFeatureVersion = FeatureVersionOf; /** * The inline server. Routes are chainable and fully type-inferred. `derive`/ * `decorate` extend the handler context (`Ctx`) for routes defined *after* them, * with full types; `Ctx` is server-only and never touches the client registry. * * app.decorate("db", db).derive((c) => ({ user: auth(c) })) * .get("/me", (c) => c.user) // c.user + c.db are typed */ export declare class Server { /** * Which copy of `@nifrajs/core` declared this type. Two copies in one build (a linked sibling repo, * a hoisting split) produce two unrelated `Server` types whose assignment error is a wall of * missing internals; the brand puts the feature version in the type itself, so `typeof app` on * hover - and `NifraFeatureVersion` in an assertion - says which core an app is talking to. * `nifra doctor` is the diagnosis that names both install paths. * * `declare` = type-only: no field is emitted, nothing is allocated per server. */ readonly __nifraCoreVersion: NifraFeatureVersion; private readonly catalog; /** WebSocket routes, matched separately at upgrade time (a GET + `Upgrade: websocket`). */ private readonly wsRouter; private wsRouteCount; /** In-process pub/sub backing `ws.subscribe(topic)` + `app.publish(topic, data)` (single-instance). * Created by the first `app.ws()` via the `@nifrajs/core/ws` runtime - `undefined` until then, so a * no-WebSocket app never constructs (or bundles) it. */ private topics; /** True once any `app.ws()` route sets `validateSend: true` - the one case where a broadcast frame * is validated (and possibly dropped) per socket, so `app.publish` must stay on the JS registry loop * rather than delegate to Bun's native `server.publish`. */ private wsHasValidatedSend; /** Bun's native topic broadcast, bound by `listen()` in native-pubsub mode; `app.publish` uses it * instead of the JS registry. `undefined` off Bun, before `listen()`, or with a validated-send route. */ private nativePublish; private readonly maxBodyBytes; private readonly protoPoisoning; /** `trustBodyFraming`: mark every `app.fetch` request as runtime-framed (see ServerOptions). */ private readonly trustBodyFraming; private readonly wsMaxPayloadBytes; private readonly requestTimeoutMs; /** Installed by the `responseContract()` plugin; `undefined` = not installed, which is the default * and the state in which a declared `response` schema stays a compile-time contract only. */ private responseContractRuntime; /** Opt-in caller-IP trust declaration; `undefined` = socket peer only, no forwarded header believed. */ private readonly clientIpTrust; private readonly acceptInboundDeadlines; private readonly maxInboundDeadlineMs; private readonly deadlineAdmissionOptions; private readonly gracefulSignals; private readonly stopHooks; private readonly fetchMounts; /** Capacity-admission gate; `undefined` = off (the request path pays nothing). */ private readonly capacityGate; private readonly onCapabilityUse; private readonly capabilityInterceptors; private readonly capabilityObservers; /** The installed effect-ledger runtime (owns the sink + per-route resolution + settle), or * `undefined` when the effect-ledger plugin is not installed. */ private effectLedgerRuntime; /** The installed idempotency runtime (owns the app-wide default store + the dedupe lane), or * `undefined` when the idempotency plugin is not installed. */ private idempotencyRuntime; /** Installed opt-in runtime for `.tool()`/`.resource()`/`.prompt()`; `undefined` until `.use(mcp())`. */ private mcpRuntime; /** Installed Node-direct renderer for direct `resolveNode()` callers; `undefined` until `.use(nodeDirect())`. */ private nodeOutcomeRuntime; /** Installed streaming runtime for `.sse()` routes; `undefined` until `.use(streaming())`. */ private sseRuntime; /** Installed WebSocket runtime for `.ws()` routes; `undefined` until `.use(websocket())`. */ private wsRuntime; private readonly logger; /** How much of an unhandled error {@link logRequestError} records. See `ServerOptions.errorLogDetail`. */ private readonly errorLogDetail; /** App-wide validation-error fallback; a route's own `schema.onValidationError` takes precedence. */ private readonly defaultOnValidationError?; private bunServer; private sealed; private readonly derives; private readonly decorations; private readonly beforeHandleHooks; private readonly afterHandleHooks; private readonly onErrorHooks; private readonly aroundHooks; private readonly onRequestHooks; private readonly onNodeRequestHooks; /** Registration-time eligibility for the Node request twin lane. */ private nodeRequestHooksComplete; private readonly onResponseHooks; private readonly onNodeResponseHooks; /** Registration-time eligibility for the Node response twin lane. */ private nodeResponseHooksComplete; /** * True once a bundle registers a RAW `onNodeResponse` twin - one handed the outcome's header * record itself rather than the case-normalizing view. Their names are contractually the wire * spelling, but registration cannot PROVE it, so such an app forgoes the all-lowercase mark and * every reader re-derives the answer, exactly as before the mark existed. */ private hasRawNodeResponseHook; private readonly onResponseFinalizedHooks; /** * Statically declared response headers, merged and prebuilt at registration; `undefined` (the * default) leaves every render path in its original shape. These are NOT response hooks - they are * folded into response construction - so declaring them keeps the fused native lanes an * `onResponse` hook would have disabled. */ private staticResponseHeaders; /** `wrapResponse` for the Web lanes: identity until static headers exist to fold into the * framework's own error/404/timeout renders, which are built outside the header init. */ private wrapWebResponse; private webResponseTimeout; /** Body/raw response hooks need a framework-payload marker; keep that decision per app. */ private responseBodyTag; private readonly responseBodyOwners; private readonly finalizeResponse; private readonly responseRequests; /** Original source → request observed by generic request hooks, including in-place mutations. */ private readonly responseSources; /** Memoized NodeRequestContext per plain-`Request` source - see {@link nodeRequestContextOf}. */ private readonly nodeContexts; /** Names of plugins/middleware already applied via `use` - for idempotent dedupe. */ private readonly appliedPlugins; /** Order-scoped evidence captured by routes registered after an assured plugin. */ private readonly activeAssurance; /** App-wide evidence from global hooks; applies retroactively to every route. */ private readonly globalAssurance; /** App-declared MCP resources / prompts (via {@link resource} / {@link prompt}), read by `nifra mcp`. */ private readonly mcpResourceList; private readonly mcpPromptList; constructor(options?: ServerOptions); private assertConfigurable; /** Register a callback awaited after graceful drain and server stop. Hung callbacks are bounded. */ onStop(fn: StopHook): this; /** * Mount a legacy fetch handler under a literal path prefix. Mounted handlers sit behind typed * routes, so routes can move one at a time while the remaining legacy surface stays live. * Mounted responses are outside Nifra's typed route and response-contract checks. */ mountFetch(path: string, handler: FetchHandler>, options?: MountFetchOptions): this; /** * Add a per-request, computed context extension. Order-scoped: it is snapshotted into every route * declared *after* this call, and reaches none declared before. One added after the last route * (e.g. on the app returned by a route-registering factory) covers nothing - see * {@link ServerOptions.unusedScopedHooks}. * * Returning a `status(...)` (or a `Response`) instead of an extension ends the request there - the * guard shape, and the cheap one: it renders as plain data on the same lane a handler's return * takes, where `throw new Response(...)` pays ~40% of the rejection lane to build the object. That * branch is control flow, so it does not widen the derived context type. */ derive MaybePromise>(fn: H): Server>, HookOutput | StatusResponseOf>>>; /** * Add a static context value. Order-scoped: captured by every route declared *after* this call, and * by none before; one added after the last route reaches nothing * (see {@link ServerOptions.unusedScopedHooks}). */ decorate(key: K, value: V): Server, HookOutput>; /** * Run before routing on the raw request. Return a `Response` to short-circuit, or a replacement * `Request` to continue routing with a rewritten method/URL/headers. Global. */ onRequest(fn: (req: Request, platform?: Platform>) => MaybePromise): this; /** * Run after validation, before the handler; a non-`undefined` return short-circuits. Order-scoped: * it is snapshotted into every route declared *after* this call and covers none declared before, so * one added after the last route reaches nothing (see {@link ServerOptions.unusedScopedHooks}). */ beforeHandle MaybePromise>(fn: H): Server>>; /** * Wrap the matched route lifecycle for subsequent routes. This is intentionally generic over the * route output, so wrappers like async context storage do not force Node's direct JSON path through * a Web `Response`. The first registered wrapper is outermost. * * Order-scoped: captured by every route declared *after* this call, none before; one added after the * last route reaches nothing (see {@link ServerOptions.unusedScopedHooks}). */ around MaybePromise) => MaybePromise>(fn: H): Server>>; /** * Asynchronously admit each subsequent `executeCapability()` call before its owned effect runs. * Interceptors receive token-only metadata plus an abort signal and must call `next()` exactly once * to admit. Returning without `next()`, timing out, aborting, or throwing fails the effect closed. */ aroundCapability(interceptor: CapabilityInterceptor, options?: AroundCapabilityOptions): this; /** Observe token-only admission/execution lifecycle events for subsequent capability routes. */ observeCapability(observer: EffectLifecycleObserver): this; /** Transform the handler's result before it is serialized. Order-scoped: captured by routes declared * *after* this call, none before; one added after the last route reaches nothing (see * {@link ServerOptions.unusedScopedHooks}). */ afterHandle MaybePromise>(fn: H): Server>>; /** Handle a thrown error; a non-`undefined` return becomes the response (else the default 500). * Order-scoped: captured by routes declared *after* this call, none before; one added after the last * route reaches nothing (see {@link ServerOptions.unusedScopedHooks}). */ onError MaybePromise>(fn: H): Server>>; /** Transform every outgoing response - success, error, 404, 405, short-circuit. Global. */ onResponse(fn: (response: Response, req: Request) => MaybePromise): this; /** * Declare response headers with NO per-request decision behind them - and pay nothing for them on * the request path. * * These are not a hook. Because the values are known at wire-up, they are folded into response * construction (one prebuilt init for JSON renders, one record merge where the request set its own * headers), so an app whose only response middleware is static keeps every fused and native lane - * `onResponse`/`onResponseHeaders` disable Bun's fused native routes and, for a full `onResponse`, * the Node direct writer. They apply to EVERY response, exactly as a response hook would: success, * error, 404/405, timeout, and short-circuit alike. * * app.responseHeaders({ "x-frame-options": "DENY", "referrer-policy": "no-referrer" }) * * They are DEFAULTS: a value the request itself produced (`c.set.headers`, or a response hook) * wins, whatever casing it used. Names are lowercased once here; a non-string value, an invalid * name, `__proto__`, or a name the render owns (`content-type`, `content-length`, * `transfer-encoding`, `set-cookie`) throws a `TypeError` at wire-up. * * ORDERING: declarations made before any response hook fold into one static record. One made AFTER * a response hook cannot - the hook may already have written that name and must keep winning - so * it registers as an ordinary `onResponseHeaders` hook instead, preserving registration order at * the cost of the static tier's speed. Declare static headers first. */ responseHeaders(record: Readonly>): this; /** Wire an already-validated lowercase record into the static tier, or - once a dynamic response * hook owns part of the header state - into a hook that runs in the right order. */ private addStaticResponseHeaders; private enableResponseBodyTagging; private responseObserverMethods; /** Observe the terminal response after all transformations. Observers are ordered and fail-open. */ onResponseFinalized(fn: (outcome: ResponseFinalization, req: Request) => MaybePromise): this; /** Enable the opt-in portable response observer methods. */ use(plugin: ResponseObserverPlugin): this & ResponseObserverMethods; /** * Apply a **context** plugin ({@link ContextPlugin}, from {@link defineContextPlugin}) - it adds `D` * to the handler context and changes nothing else, so the route registry `R` and the existing context * `Ctx` thread through untouched. Like the identity overload above, this exists because the generic * `(app: this) => Out` overload below cannot infer through a *generic* plugin signature: it erases the * plugin's own type parameters to their constraints, which silently widens `R` to `Registry`. */ use(plugin: ContextPlugin): Server; /** * Apply a type-**identity** plugin ({@link IdentityPlugin}, from {@link defineIdentityPlugin}) - it * registers routes/hooks but doesn't change the types, so this returns `this` with the route registry * and context fully intact. This overload exists specifically so a named identity plugin (e.g. * `@nifrajs/better-auth`) threads the registry: its `& { pluginName }` intersection would otherwise * defeat the generic inference of the transforming overload below and collapse the result to `any`. */ use(plugin: IdentityPlugin): Server; /** * Apply a **plugin function** - `(app) => app`, typically built with {@link definePlugin}. It's * called with `this` and its result is returned, so an inline plugin's `derive`/`decorate` thread * the added context to handlers defined after `use` (the overload is generic over the concrete * `this`). A named plugin already applied is skipped (idempotent dedupe). * * If the plugin's return type is unpinned - `Server`, as a hand-rolled `(app) => app` or a * `NifraPlugin` infers (e.g. an auth plugin whose own types collapsed) - this * returns the non-callable {@link PluginTypeCollapsed} instead of silently widening the whole typed * client to `any`. `.get()`/`.post()` then fail right here rather than surfacing as `any` hundreds of * lines away. Fix at the plugin: build it with {@link defineIdentityPlugin}/{@link defineContextPlugin}, * or pin its input server type. This mirrors the guard {@link definePlugin} already applies. */ use(plugin: (app: this) => Out): [ServerTypeUnpinned] extends [true] ? PluginTypeCollapsed : Out; /** * Apply a {@link Middleware} bundle - wire each hook it provides to its lifecycle point. Returns * `this` (no context-type merging); call it before the routes its `beforeHandle`/`afterHandle` * should cover (those are order-scoped; `onRequest`/`onResponse` are global). A bundle applied after * the last route reaches nothing - {@link ServerOptions.unusedScopedHooks} logs that at seal. A named * bundle already applied is skipped (idempotent). */ use(mw: M): Server>; use(mw: Middleware): this; get>(path: Path, schema: S, handler: H): Server, HookOutput>>, Ctx, HookOutput>; get>(path: Path, handler: H): Server, OutputOf, HookOutput>>, Ctx, HookOutput>; /** * Register a **typed SSE route** - a GET endpoint streaming `text/event-stream` whose event * payloads are contracted by `schema.sse`. The handler receives the validated context plus a * {@link TypedSSEStream}: `stream.send(event)` is compile-time-checked against the schema and * JSON-serialized into the SSE `data:` field. The typed client sees the marker and grows a * `.subscribe(onEvent)` for the route with the same payload type - end-to-end typed streaming. * * import { streaming } from "@nifrajs/core/sse" // .use(streaming()) enables .sse() * const app = server().use(streaming()).sse("/feed", { sse: t.object({ id: t.integer(), title: t.string() }) }, * async (c, stream) => { * stream.send({ id: 1, title: "hello" }) // typed * await waitForDisconnect(stream.signal) * }, * { keepAlive: 15_000 }) * * // client: const off = api.feed.subscribe((post) => console.log(post.title)) * * `init` passes through to the underlying {@link sse} helper (`keepAlive`, extra headers). The * connection closes when the handler resolves, `stream.close()` runs, or the client disconnects * (`stream.signal`). Query/body schemas validate exactly as on any other route. */ sse(path: Path, schema: S, run: (context: Context & Ctx, stream: TypedSSEStream>) => void | Promise, init?: SSEInit): Server>, Ctx, HookOutput>; post>(path: Path, schema: S, handler: H): Server, HookOutput>>, Ctx, HookOutput>; post>(path: Path, handler: H): Server, OutputOf, HookOutput>>, Ctx, HookOutput>; put>(path: Path, schema: S, handler: H): Server, HookOutput>>, Ctx, HookOutput>; put>(path: Path, handler: H): Server, OutputOf, HookOutput>>, Ctx, HookOutput>; patch>(path: Path, schema: S, handler: H): Server, HookOutput>>, Ctx, HookOutput>; patch>(path: Path, handler: H): Server, OutputOf, HookOutput>>, Ctx, HookOutput>; delete>(path: Path, schema: S, handler: H): Server, HookOutput>>, Ctx, HookOutput>; delete>(path: Path, handler: H): Server, OutputOf, HookOutput>>, Ctx, HookOutput>; /** * Declare an **MCP tool** an agent can call (via `nifra mcp`, or a mounted MCP endpoint): a typed * `POST /_nifra/tool/` route whose `input`/`output` schemas contract the call and surface in * `tools/list`. Requires `.use(mcp())` - without it, `.tool()` is a registration error, so an * ordinary HTTP app never bundles the MCP wiring. Siblings: {@link resource}, {@link prompt}. * * import { mcp } from "@nifrajs/core/mcp" * const app = server().use(mcp()).tool( * "search", * { description: "Search posts", input: t.object({ q: t.string() }) }, * ({ q }) => findPosts(q), * ) */ tool, ctx: Context & Ctx) => MaybePromise : unknown>>(name: Name, config: S, handler: H): Server, HookOutput>>, Ctx, HookOutput>; /** * Declare an MCP **resource** - read-only data an agent can fetch through `nifra mcp` (app config, a * generated document, …). `read` runs in the app process, so capture whatever app state it needs in the * closure. `uri` is the MCP resource identifier (e.g. `"myapp://config"`). The sibling of {@link tool} * for the resource half of MCP. */ resource(uri: string, config: { readonly name: string; readonly description?: string; readonly mimeType?: string; }, read: McpResourceDescriptor["read"]): Server; /** * Declare an MCP **prompt** - a reusable prompt template an agent can fetch through `nifra mcp`. * `handler` receives the caller's arguments and returns the rendered messages. */ prompt(name: string, config: { readonly description: string; readonly arguments?: readonly PromptArgument[]; }, handler: McpPromptDescriptor["handler"]): Server; /** The MCP resources declared via {@link resource} - enumerated by `nifra mcp`. */ mcpResources(): readonly McpResourceDescriptor[]; /** The MCP prompts declared via {@link prompt} - enumerated by `nifra mcp`. */ mcpPrompts(): readonly McpPromptDescriptor[]; /** * Register a **WebSocket** route. The connection upgrades on a `GET` to `path` carrying * `Upgrade: websocket`; the optional `handler.upgrade(c)` runs in the request context first and may * reject (return a `Response`) or seed per-connection `ws.data`. WebSockets are served by the * adapter (`listen()`, `@nifrajs/node`, `@nifrajs/deno`, `toFetchHandler`) - not by bare `app.fetch`, which * has no socket (a WS path through `app.fetch` is a normal HTTP response). * * The route also enters the type-level registry (under the pseudo-method `"WS"`), so the typed * client grows a `.ws()` handle for it: `messageSchema` types what the client may `send`, * `sendSchema` types the frames it receives. Passing explicit type arguments (`ws(…)`) * defeats path-literal inference and skips the registry entry - the route still serves, it is just * invisible to `client`; prefer typing `data` via `upgrade()`'s return. * * app.ws("/chat", { open: (ws) => ws.send("hi"), message: (ws, data) => ws.send(data) }) */ ws(path: Path, handler: WebSocketHandler, Schema, Send>): string extends Path ? Server : Server>, Ctx, HookOutput>; /** * Broadcast `data` to every WebSocket connection subscribed to `topic` (via `ws.subscribe(topic)`). * In-process and **single-instance** (see {@link TopicRegistry}) - a multi-instance deploy must bridge * an external fan-out (Redis, a Durable Object) to this. A no-op when nobody is subscribed. */ publish(topic: string, data: string | ArrayBufferView | ArrayBuffer): void; private route; /** * Low-level route registration shared by the inline builder and `implement()`. * Captures the server's current `derive`/`decorate` chain into the route - this * is the "compiled", order-scoped per-route chain. */ register(method: Method, path: string, schema: RouteSchema | undefined, handler: (context: never) => unknown): void; /** Register a contract/group route batch atomically. Every route captures the same current chain it * would capture through {@link register}; no route becomes visible unless the full batch validates. */ registerBatch(routes: readonly { readonly method: Method; readonly path: string; readonly schema: RouteSchema | undefined; readonly handler: (context: never) => unknown; }[]): void; private prepareRoute; /** The idempotency lane's bridge back into the normal matched lanes, resolved to a concrete Response * (the lane buffers the body, then replays it through the route's real validation + handler). */ private idempotencyRunLanes; /** @internal Symbol-keyed install seam for the `responseContract()` plugin. Off the public typed surface. */ [INSTALL_RESPONSE_CONTRACT](runtime: ResponseContractRuntime): void; /** @internal Symbol-keyed install seam for the response observer plugin. */ [INSTALL_RESPONSE_OBSERVER](runtime: ResponseObserverRuntime): ResponseObserverMethods; /** @internal Symbol-keyed install seam for the `idempotency()` plugin. Off the public typed surface. */ [INSTALL_IDEMPOTENCY](runtime: IdempotencyRuntime): void; /** @internal Symbol-keyed install seam for the `mcp()` plugin. Off the public typed surface. */ [INSTALL_MCP](runtime: McpRuntime): void; /** @internal Symbol-keyed install seam for the `nodeDirect()` plugin. Off the public typed surface. */ [INSTALL_NODE_DIRECT](runtime: NodeOutcomeRuntime): void; /** @internal Symbol-keyed install seam for the `streaming()` plugin. Off the public typed surface. */ [INSTALL_SSE](runtime: SseRuntime): void; /** @internal Symbol-keyed install seam for the `websocket()` plugin. Off the public typed surface. */ [INSTALL_WS](runtime: WsRuntime): void; /** * Merge another server's routes into this one - the composition escape hatch for large apps. * * WHY: the fluent chain accumulates one type-alias level per route, and TypeScript resolves * that stack in one recursion - a single chain hits TS2589 at ~95 routes. Groups keep every * chain short: build each domain (`listings`, `agents`, …) as its own `server()` (its registry * resolves independently), then `app.merge(listings).merge(agents)` - each merge adds ONE level * regardless of group size. 300+ routes stay fully typed (see many-routes.test-d.ts). The * other escape hatch is contract-first `implement()`, whose registry is a single object type. * * Semantics: merged routes keep the chains captured where they were DEFINED - the group's * `derive`/`decorate`/`beforeHandle`/`afterHandle`/`onError`/`around` apply to its routes * exactly as they did standalone, so a group wires its own plugins. The same locality rule * covers the group's `onRequest` hooks: they run only for requests that route to the GROUP's * routes (a `bodyLimit()` mounted on an uploads group must not start gating the whole app * because the app composed it in), and the group's global assurance follows its hooks onto * exactly those routes. A group with hooks but NO routes is a middleware bundle - its hooks * can only mean app-wide intent, so they are appended globally, unchanged. Response-side hooks * (`onResponse`/`onResponseFinalized`) are appended to this server's. This server's * route-scoped chains do NOT retroactively wrap merged routes (order-scoped, like routes * registered before a `derive`). Fail closed: a path+method collision throws * `RouteConfigError` at merge time, and a group with WebSocket routes is refused (register * those on the parent). */ merge(other: Server): Server; /** A fused renderer closes over runtime services to keep its seven-argument JSC fast path. Merging * rebinds that closure once to the executing server; generic plans already receive the runtime. */ private bindFusedRuntime; /** * Enumerate the registered routes (method, path, input schemas), in registration * order. Powers `toOpenAPI` and other introspection; the router trie itself no * longer holds the original patterns. */ routes(): ReadonlyArray; /** * Resolve a `Request` to a `Response` - the whole lifecycle, testable without a port. The * optional `platform` carries edge inputs (`env`, `waitUntil`); edge adapters pass it, and * Bun/Node/Deno omit it (then `c.env` is `undefined` and `c.waitUntil` runs fire-and-forget). */ fetch(req: Request, platform?: Platform>): MaybePromise; private fetchSource; private fetchSourceInner; /** Web response path when Bun already matched the route. The lifecycle and response hooks remain * exactly the same as {@link fetchSource}; only portable URL scanning + trie lookup are skipped. */ private fetchMatched; private fetchMatchedInner; /** * Run `produce` under the capacity gate: admit → run → release exactly once when the response is * produced (or the run throws). Only reached when {@link capacityGate} is set, so the off path pays * nothing. The slot is held for the duration of handler execution, not the streaming of the body - * capacity here bounds concurrent *work*, matching how in-flight is counted. */ private admitGated; private runAdmitted; /** * Select a mounted handler's native Node lane without materializing a Web Request. This is an * adapter seam, not a second lifecycle: any app-wide behavior that the ordinary mount path would * run makes the capability ineligible and the caller falls back to `resolveNodeSource`. Typed * routes keep precedence exactly as `routeAndRun` does, including the existing mount behavior for * a path whose registered route rejects this method. */ [RESOLVE_NODE_MOUNT](source: RequestSource): NativeMountSelection | undefined; /** * Like {@link fetch}, but renders a plain-data result **without** building a Web `Response` - the * `@nifrajs/node` adapter serializes the returned primitives straight to the socket, skipping the undici * `Response` build + body drain (the bulk of the Node bridge cost, measured ≈4µs/req). A handler that * returns a `Response`, an error/short-circuit, or a response hook that replaces/consumes the buffered * body stays on the full Web path; an in-place response hook can still use the direct writer. Same * lifecycle as {@link fetch} (body cap, validation, hooks all run); only the final render differs. */ resolveNode(req: Request, platform?: Platform>): MaybePromise; resolveNodeSource(source: RequestSource, platform?: Platform>, suppliedRuntime?: NodeOutcomeRuntime): MaybePromise; /** Run generic Web response middleware while retaining direct writes for untouched buffered bodies. */ private finishNodeWebResponse; /** True only when every transforming Web response hook has a header-only Node equivalent. */ private canUseNodeResponseHooks; /** Apply paired native hooks to data outcomes; preserve the complete Web hook pipeline for Response outcomes. */ private finishNodeResponse; private withNodeResponseHeaders; /** Synchronous until a native response hook actually returns a Promise. */ private applyNodeResponseHooks; private continueNodeResponseHooks; /** * Resolve a WebSocket upgrade - the seam every serving adapter uses. Returns `pass` (not a WS * upgrade for a registered route → handle as normal HTTP), `reject` (a WS route matched but * `upgrade()` rejected, or the path was malformed → return `response`), or `upgrade` (perform the * runtime upgrade, then dispatch the native socket's events to `handler`, seeding `ws.data` with * `data`). Runs the route's `upgrade(c)` guard in a real request context. Synchronous unless * `upgrade()` is async; a throw rejects with a flat 500 (no detail leaked). */ resolveWebSocketUpgrade(req: Request, platform?: Platform>): MaybePromise; /** Bun `fetch` when WS routes exist: try a WS upgrade first, else run the normal HTTP lifecycle. * `undefined` ⇒ Bun owns the upgraded socket; a `Response` ⇒ a normal reply or a rejected upgrade. * (The socket dispatch itself lives in `ws-bun.ts`, loaded via `@nifrajs/core/ws`.) */ private bunFetchWithWebSocket; /** * The shared lifecycle, generic over how the final value is rendered: `finalize` turns a handler's * result + `set` into the output `T` (`toResponse` → a Web `Response`; `toNodeOutcome` → node-direct * primitives), `wrapResponse` lifts an early/error `Response` into that same `T`, and `onTimeout` * produces the 503. The Web `fetch` and `resolveNode` are thin callers over this one routing + * context + lifecycle implementation - no duplication across the trust boundary. */ /** Apply the `clientIp` trust declaration to the adapter's raw socket peer, returning a platform * whose `clientIp` is the derived caller. Only called when a trust declaration is configured. */ private deriveClientIp; private dispatch; /** Node-native request hook walk. A header-only hook can inspect the lazy source without forcing a * Web `Request`; arbitrary request rewrites and full Web hooks use {@link runWithOnRequest}. */ private runWithNodeRequest; private continueNodeRequest; private canUseNodeRequestHooks; /** * onRequest short-circuit path. Synchronous as long as every hook returns synchronously (the * common case - e.g. CORS returning `undefined` for a non-preflight request): an `async` version * here put EVERY request of any app with one onRequest hook onto the promise machinery, profiled * at ~13% of a realistic request. The first hook that returns a Promise hands the REMAINING * hooks to the async continuation; behavior is identical. */ private runWithOnRequest; /** Async tail of {@link runWithOnRequest}: applies the first awaited hook's outcome, then runs * the remaining hooks (awaiting freely - we're already async here). */ private continueOnRequest; private takeResponseRequest; /** * The NodeRequestContext for a source, MEMOIZED per source so the request twins and the response * twins receive the exact same object within one request - that identity is the documented * contract stateful twins key their WeakMaps on. An adapter source (which already speaks the * interface) is returned as-is; a plain `Request` source (a direct `resolveNode` caller) gets one * cached wrapper. */ private nodeRequestContextOf; /** Preserve the request visible to generic onRequest hooks for paired native response hooks. */ private takeNodeResponseRequest; /** * Route → build context → run. Synchronous through to the handler for a **bare** route * (selected by its compiled plan), so a sync handler produces its result with zero promise allocations; * routes with validation/hooks keep the full compiled route program, unchanged. */ private routeAndRun; private fetchMount; /** Run a route that has already been matched by the runtime or Nifra's portable router. */ private runMatched; /** Supply request-specific deadline state to the route's precompiled execution plan. */ private runMatchedLanes; /** @internal Symbol-keyed install seam for the `effectLedger()` plugin. Off the public typed surface. */ [INSTALL_EFFECT_LEDGER](runtime: EffectLedgerRuntime): void; /** The narrowest bare route: a syntactic `() => ...` handler cannot observe the context argument, so * successful requests can skip allocating `RequestContext`. Errors still allocate one for logging. */ private runContextlessBare; private contextlessBareError; /** * The synchronous fast path selected by a route's execution plan: apply static decorations, call the * handler, render the result - **no `await`** unless the handler itself returns a promise. It mirrors * the same error boundary as the compiled route program (which a bare route would otherwise no-op through) and shares * {@link logRequestError}; a bare route has no `onError` hooks, so error handling is fully synchronous * (a thrown `Response` is control flow; anything else is a logged flat 500). This is where nifra skips * the per-request async-frame tax - the same win codegen routers get, but without `eval`. */ private runBare; /** Bare-route error rendering - identical to the compiled route program's catch minus the (absent) onError * loop: a thrown `Response` is returned as deliberate control flow; anything else is logged + 500. */ /** * Build a route's fused Web renderer. Composition happens once at * registration; the returned closure is what every request to the route runs. Behavior is * byte-identical to the generic `runBare`/`runContextlessBare` + `toResponse` pair - same * decoration order, same error routing (thrown `Response` = control flow; anything else logs and * 500s), same respond semantics (the lifecycle parity suite pins it). */ private buildFusedWeb; /** Compile the eligible body-only route's parser → validator → handler continuation once. The * bounded parser remains shared with the generic lane; only the route-invariant entry lookups and * lifecycle dispatch disappear from the common synchronous-validator/synchronous-handler case. */ private buildFusedBodyRunner; /** Web adapter wrapper for the shared body runner. Node passes its native finalizer directly through * the execution plan, while Web needs the live context to preserve lazy `c.set` controls. */ private buildFusedBodyWeb; /** The fused Web lane for a route whose only lifecycle step is a query schema: parse + validate + * handler + respond in one closure, no lifecycle promise when the validator and handler are sync. * Eligibility is decided at registration (see `fusedQuery` in {@link register}); the semantics here * are exactly `runQueryOnly`'s for that eligible shape - invalid input returns * `validationError(issues)` (recovery hooks disqualify the route from this lane), a thrown * `Response` passes through, anything else logs and returns a flat 500, and an async validator * falls to a then-chain with the same steps. */ private buildFusedQueryWeb; /** * Compile a route's `derive + before` lifecycle to a single closure: the per-request walk through * `runSync` → `runHooksSync` is a stack of 3 frames and 2 `Object.assign`s for what is by far the * most common middleware shape (auth/trace/id derivation, an auth/policy `beforeHandle`). Folding * the same steps into one closure hits the same monomorphic inline-cache site as `buildFusedWeb`, * and `Object.assign(ctx, deriveResult)` becomes one extra property write on the context's already * hot hidden class. Behavior is byte-identical to the compiled route program: the same thrown-value * contract (`Response` is control flow, anything else logs + 500), the same `c.set` semantics, the * same `fusedRespond` for the rendered body. The `LifecycleExecutionLane` guarantee * (`schema.params === undefined && schema.headers === undefined && derives === 1 && * beforeHandle === 1 && afterHandle === 0 && onError === 0 && !hasResponseContract && * !hasDecorations`) is enforced by `selectRouteLanes`; this lane is selected ONLY when all of those * hold. A `query` schema (the route declares `{ query }` alongside the hooks) is validated FIRST, * mirroring the compiled route program's stage order; the fused builder runs validate → derive → * before → handler in one frame. */ private buildFusedDeriveBefore; /** * Same shape as `buildFusedDeriveBefore`, plus one `afterHandle` step. The closure runs (validate) → * derive → before → handler → after in one frame, the shape the realistic middleware route lands on * when it also wants a response transform (e.g. attaching a request id to a successful payload's * headers). `LifecycleExecutionLane` and `selectRouteLanes` guarantee `afterHandle === 1`, the * throw contract is the same as the generic program, and the `after` step is the final transform + * respond the generic program already does. */ private buildFusedDeriveBeforeAfter; /** * Body + lifecycle hooks fused: parse → validate → derive → before → handler → (after) in one * closure, the same win `buildFusedBodyRunner` gives for the no-hooks body shape. The shape is * `lifecycleHookLane` PLUS a body schema with no other validation conflicts; the lane is selected * ONLY when `schema.body !== undefined && schema.query/params/headers === undefined && * schema.onValidationError === undefined && !defaultOnValidationError && !hasIdempotency && * !hasLedger && derives === 1 && beforeHandle === 1 && afterHandle <= 1 && onError === 0 && * !hasResponseContract && !hasDecorations && around === 0`. The same single `querySchema === undefined` * trick used in the no-body lane lets the build path stay simple: a query + body route falls back * to the generic program (this is rare in practice - validated query + validated body is usually a * "search results" route with a middleware chain on top, and the generic lane is fine there). */ private buildFusedBodyDeriveBeforeAfter; private bareError; /** * Keep the ordinary thrown-error response independent from the caught value. Deliberate Response * control flow is handled by `renderBareError`; every other throw gets this fixed public envelope. */ private internalErrorResponse; private runWithAround; /** Execute the registration-compiled general stage program. */ private runProgram; /** Bind a compiled validation stage to its stable schema and the existing recovery contract. */ private validateProgramStage; /** Preserve the existing bounded body reader and its fail-closed framing/prototype checks. */ private readProgramBody; /** Keep thrown Response/status and redacted 500 semantics in one existing error boundary. */ private handleProgramError; /** Finish a successful program result through response-contract enforcement and finalization. */ private finishProgramResult; private runAround; private runBodyOnly; /** Validate + run the handler for the bodyOnly path - shared by the inline fast path and the * streaming fallback. A method (not per-request closures) so the hot path allocates nothing * beyond the one `.then` continuation. */ private finishBodyOnly; private executeHandler; private handleValidationErrorRecovery; private applyBodyValidation; private runQueryOnly; /** Validate-result → set `ctx.query` → run handler. A method (not a per-request closure), the * query analogue of {@link applyBodyValidation}. */ private applyQueryValidation; /** * Thread the response through each global `onResponse` hook. Stays SYNCHRONOUS until a hook * actually returns a Promise - an `async` version forced a promise + microtask on EVERY response * of any app with an onResponse hook (cors/securityHeaders/etag/timing all use onResponse), the * same ~13%/req tax the onRequest walk was de-async'd to avoid. The first async hook hands the * rest to {@link continueOnResponse}. */ private applyOnResponse; private applyOnResponseAndFinalize; private completeResponseFinalization; private failResponseFinalization; /** Notify terminal observers in order while isolating both sync and async failures. */ private notifyResponseFinalized; /** Async tail of {@link applyOnResponse}: runs the remaining hooks once one has gone async. */ private continueOnResponse; /** * Bound the response time. On timeout we abort `ctx.signal` (so cancellation-aware * handlers can bail) and return 503; the in-flight work keeps running but its * result is discarded - JS can't forcibly cancel a promise. */ private withTimeout; private finishLifecycleContract; private finishContractOutcome; /** Synchronous by design, and `MaybePromise` rather than `Promise` for the same reason: control flow * out of a lifecycle stage must not cost a promise. An `async` method allocates one and defers the * result a microtask even when every branch it takes returns immediately, which is the common case * here - a thrown `status(...)`/`Response`, or a route with no `onError` hook at all. Only the hook * loop, which may await, drops into {@link runErrorHooks}. */ private handleLifecycleError; /** The `onError` chain, split off {@link handleLifecycleError} so its `await` costs only the routes * that registered a hook. A hook may return a custom response; otherwise the default 500 stands. */ private runErrorHooks; /** Log an unhandled request error to the (redacting) logger - shared by the compiled route program and the * bare fast path ({@link bareError}) so both record the same fields. Never throws; never leaks. */ private logRequestError; private readAndValidateBody; /** Apply validation and its recovery hook on the generic lifecycle lane. Recovery is completed * before derives/beforeHandle run, matching the body-only and query-only execution lanes. */ private applyLifecycleValidation; private finishLifecycleValidationRecovery; /** Adapt one route's compiled execution plan to Bun's already-matched request shape. Route * semantics remain in the plan; this closure only supplies native params and deadline fallback. */ private compileBunNativeHandler; /** Compile portable route registrations into Bun's native route table. Apps with request-rewrite * hooks or WebSockets retain the single portable dispatcher because those features must run before * route selection/upgrade. Named wildcards also stay on the fallback until Bun exposes their raw * capture semantics; static and `:param` routes take the native lane. */ private buildBunNativeRoutes; /** * Start a `Bun.serve` instance bound to `port` (use `0` for an ephemeral port). * * `reusePort` sets `SO_REUSEPORT` so **multiple processes can bind the same port** and the kernel * load-balances connections across them - the standard way to use every core (Bun is * single-threaded per process). Spawn one process per core, each calling * `app.listen(PORT, { reusePort: true })`; see `examples/cluster.ts`. Every process must opt in, * and all of them must be the same app. Linux balances ~evenly; macOS accepts the flag but may * favor one socket (fine for dev, measure on Linux for production numbers). * * `hostname` is the bind address, defaulting to Bun's `0.0.0.0` (every interface). Pass * `"127.0.0.1"` to bind loopback only - an admin surface, a sidecar, or anything that must not be * reachable off the box. Omitting it when you meant to restrict is a real exposure, so it is a * first-class option rather than something a caller has to drop down to `Bun.serve` for. * * `idleTimeoutSec` is how long Bun tolerates a connection with no bytes moving before it closes * the socket. Bun's default is **10 seconds**, and a request whose handler is still working sends * nothing - so any endpoint slower than 10s (a render, an export, a long upstream call) has its * connection cut mid-flight regardless of `requestTimeoutMs`. Apps with such endpoints must raise * this above their slowest expected response, which is why it is a first-class option and not a * reason to drop down to `Bun.serve`. `0` disables the timeout entirely; max 255. */ listen(port: number, options?: { readonly reusePort?: boolean; readonly hostname?: string; readonly idleTimeoutSec?: number; }): RunningServer; /** * Gracefully stop: wait for in-flight requests to finish (up to `drainMs`), then * issue a single terminal stop - graceful if everything drained, forced if * stragglers remain. Safe to call when not listening. * * The Bun semantics: poll `pendingRequests` (awaiting * `stop()`'s promise drops in-flight requests), and decide graceful-vs-forced in * ONE call (Bun can't escalate an already-graceful `stop()` to a forced close). * New connections may be accepted during the drain window; in a real deploy the * load balancer has already stopped routing here, and `drainMs` bounds it. */ stop({ drainMs }?: { drainMs?: number; }): Promise; private installSignalHandlers; } /** * Create a new {@link Server}. Pass an `Env` to type the platform bindings - `server()` makes * `c.env: Env` in every handler + middleware, and types the `env` argument of `app.fetch` / * `toFetchHandler`. Omit it and `c.env` is `unknown` (validate/cast before use). */ export declare function server(options?: ServerOptions): Server; //# sourceMappingURL=server.d.ts.map