import { admitDeadline, createRequestBudget, createUnboundedRequestBudget, type DeadlineAdmissionOptions, NIFRA_DEADLINE_HEADER, type RequestBudget, } from "../budget.ts" import type { EffectLifecycleObserver } from "../effect-lifecycle.ts" import { FrameworkError, RouteConfigError } from "../errors.ts" // Type-only (erased at build): the package root re-exports this module, so a value import would cycle. import type { Version } from "../index.ts" import { type AroundCapabilityOptions, type CapabilityInterceptor, type CapabilityUseEvent, DEFAULT_CAPABILITY_INTERCEPTOR_TIMEOUT_MS, type RegisteredCapabilityInterceptor, } from "../internal/capability-runtime.ts" import { type AssuranceDeclaration, assuranceDeclarationsOf, assuranceEvidenceFor, } from "../internal/route-assurance.ts" import { type CatalogRoute, RouteCatalog } from "../internal/route-catalog.ts" import { compileRouteOptions } from "../internal/route-compiler.ts" import { compileRouteExecutionPlan, type FusedBodyRunner, type FusedWebRunner, type InternalHandler, type RawAfterHandle, type RawAround, type RawBeforeHandle, type RawDerive, type RawErrorHandler, type RouteEntry, } from "../internal/route-execution.ts" import type { RouteProgramStage } from "../internal/route-program.ts" import { compileRouteProgram, executeRouteProgram } from "../internal/route-program.ts" import { isSameOriginRequest } from "../internal/same-origin.ts" import type { ResponseObserverPlugin } from "../response-observer.ts" import { decodeRouteParams } from "../router/pattern.ts" import { EMPTY_PARAMS, type Method, Router } from "../router/router.ts" import type { InferOutput, StandardIssue, StandardResult, StandardSchemaV1, } from "../schema/standard.ts" import { emitRequestErrorLog, renderBareError } from "./bare-error-lane.ts" import { assertByteLimit, markTransportCap, markTrustedBodyFraming, type RawBodyReaders, UNLIMITED_BODY_BYTES, } from "./body.ts" import { type ClientIpTrust, resolveClientIp } from "./client-ip.ts" import type { Context, Platform, ResponseControls, RouteSchema } from "./context.ts" import { hasLowercaseHeaderKeysMark, headerKeysAllLowercase, markLowercaseHeaderKeys, } from "./header-case.ts" import { headerObjectOf } from "./headers.ts" import { jsonError, pathnameOf, plainError, type UrlParts, urlPartsOf } from "./http.ts" import { type NodeServeOutcome, withStaticNodeHeaders } from "./node-outcome.ts" import type { NodeOutcomeRuntime, NodeRequestContext, NodeRequestHook, NodeResponseContext, NodeResponseHook, ResponseBodyHook, ResponseBodyReplacement, ResponseHeadersHook, ResponseHeadersView, } from "./node-outcome-hook.ts" import { type QueryValue, queryObjectOf, searchOf } from "./query.ts" import { RequestContext, readBodyFramed } from "./request-context.ts" import { applyStaticResponseHeaders, buildStaticResponseHeaders, fusedRespond, fusedRespondNoSet, toResponse, } from "./respond.ts" // Type-only: erased, so the kernel never pulls the lane's implementation into a bundle that does not // install the plugin. The value side arrives through the symbol-keyed install seam. import type { ResponseContractRuntime } from "./response-contract-lane.ts" import type { ResponseObserverMethods, ResponseObserverRuntime, } from "./response-observer-runtime.ts" import { CONTEXT_SEARCH, CONTEXT_SET, EMPTY_RESPONSE_CONTROLS, getNeverAbortSignal, getUnboundedRequestBudget, type HandlerResult, headerOf, isResponseResult, type ResponseResult, requestOf, type StatusResponse, } from "./runtime-core.ts" import { normalizeStaticResponseHeaders, type StaticResponseHeaders } from "./static-headers.ts" import { plainValidationError } from "./validation.ts" // NodeServeOutcome (the nifra<->node bridge render form) now lives in `./node-outcome.ts`; re-exported // so existing importers keep resolving it from the server module. export type { NodeRequestContext, NodeRequestHook, NodeResponseContext, NodeResponseHook, NodeServeOutcome, ResponseBodyHook, ResponseBodyReplacement, ResponseHeadersHook, ResponseHeadersView, } import type { IdempotencyRuntime } from "./idempotency-lane.ts" import { INSTALL_EFFECT_LEDGER, INSTALL_IDEMPOTENCY, INSTALL_MCP, INSTALL_NODE_DIRECT, INSTALL_RESPONSE_CONTRACT, INSTALL_RESPONSE_OBSERVER, INSTALL_SSE, INSTALL_WS, NODE_NATIVE_MOUNT, RESOLVE_NODE_MOUNT, } from "./install.ts" import type { EffectLedgerRuntime } from "./ledger-lane.ts" import { jsonLogger, type Logger } from "./logger.ts" import type { McpRuntime } from "./mcp-hook.ts" import type { ContextPlugin, IdentityPlugin, PluginTypeCollapsed, ServerTypeUnpinned, } from "./plugin.ts" import type { AddRoute, EmptyRegistry, OutputOf, Registry, RouteInfoFor, WsRouteInfoFor, } from "./registry.ts" import type { AdmissionController, AdmissionDecision, FetchHandler, McpPromptDescriptor, McpResourceDescriptor, Middleware, MountFetchOptions, PromptArgument, PromptMessage, ResponseFinalization, RouteDescriptor, RunningServer, ServerOptions, StopHook, ToolAnnotations, } from "./server-types.ts" import type { SSEInit, TypedSSEStream } from "./sse.ts" import type { SseRuntime } from "./sse-hook.ts" import type { TopicRegistry, WebSocketContext, WebSocketHandler, WebSocketUpgradeOutcome, } from "./websocket.ts" import type { BunWsData } from "./ws-bun.ts" import type { WsRuntime } from "./ws-hook.ts" 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 type ContextlessHandler = () => MaybePromise const functionToString = Function.prototype.toString const CONTEXTLESS_ARROW = /^(?:async\s*)?\(\s*\)\s*(?::[\s\S]*?)?=>/ /** 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 // Writable: the lifecycle replaces it with the validated/coerced value when a `params` schema is // declared (handlers still see it `readonly` via the public `Context` interface). 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 } /** Broad shape so the implementation signature is compatible with both typed overloads. */ type ErasedHandler = (ctx: never) => MaybePromise export type OnRequestResult = Response | Request | undefined type RawOnRequest = (req: Request, platform?: Platform) => MaybePromise type RawOnResponse = (response: Response, req: Request) => MaybePromise type RawOnResponseFinalized = (outcome: ResponseFinalization, req: Request) => MaybePromise /** A registered WebSocket route - just its handler; matching reuses {@link Router} under the GET verb. */ interface WsEntry { readonly handler: WebSocketHandler } /** Structural view of the Bun `Server` the `fetch` 2nd arg exposes (`upgrade` + the socket peer). */ interface BunUpgradeServer { upgrade(request: Request, options?: { data?: BunWsData }): boolean requestIP(request: Request): { readonly address: string } | null } type MountedFetchHandler = (request: Request, platform?: Platform) => MaybePromise /** 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 } interface FetchMount { readonly path: string readonly handler: MountedFetchHandler readonly stripPrefix: boolean } /** The socket peer Bun observed, as a `Platform` for the request lifecycle (`undefined` if unknown). * Typed structurally on `requestIP` alone so any Bun `Server` (WS or not) satisfies it. */ function bunPeerPlatform( server: { requestIP(request: Request): { readonly address: string } | null }, req: Request, ): Platform { // Bun's requestIP() is surprisingly expensive (~20 us on the SSR benchmark machine). Keep the // documented raw-peer c.clientIp behavior, but resolve it lazily: most routes never read c.clientIp, // and paying for the socket lookup on every request erased Bun's native HTTP advantage. A getter also // preserves middleware that inspects the platform argument directly and trust-mode routes, which // resolve the value in deriveClientIp before the handler runs. let resolved = false let address: string | undefined return { get clientIp(): string | undefined { if (!resolved) { resolved = true address = server.requestIP(req)?.address } return address }, } } type BunNativeHandler = (request: Request) => MaybePromise type BunNativeMethodTable = Partial> type BunNativeRoutes = Record type BunRequestWithParams = Request & { readonly params?: Record } const WS_PASS: WebSocketUpgradeOutcome = { kind: "pass" } /** `app.ws()` (and everything downstream of it) needs the runtime `@nifrajs/core/ws` registers. */ function requireWsRuntime(runtime: WsRuntime | undefined): WsRuntime { if (runtime === undefined) { throw new FrameworkError( "WS_RUNTIME_MISSING", "app.ws() needs the WebSocket runtime, which ships as an opt-in plugin so no-WebSocket apps stay lean. Add `.use(websocket())` from `@nifrajs/core/ws` before declaring WS routes.", ) } return runtime } function requireSseRuntime(runtime: SseRuntime | undefined): SseRuntime { if (runtime === undefined) { throw new FrameworkError( "SSE_RUNTIME_MISSING", "app.sse() needs the streaming runtime, which ships as a subpath so non-SSE apps stay lean. Add `.use(streaming())` (from `@nifrajs/core/sse`) at your server setup.", ) } return runtime } function requireMcpRuntime(runtime: McpRuntime | undefined): McpRuntime { if (runtime === undefined) { throw new FrameworkError( "MCP_RUNTIME_MISSING", "MCP declarations ship as an opt-in runtime so ordinary HTTP apps stay lean. Add `.use(mcp())` (from `@nifrajs/core/mcp`) at your server setup.", ) } return runtime } /** 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< Path extends string, S extends RouteSchema = RouteSchema, Ctx = EmptyContext, > = (ctx: Context & Ctx) => MaybePromise> // Route/option/descriptor + middleware-bundle types now live in `./server-types.ts`; re-exported so // existing importers keep resolving them from the server module. export type { AdmissionController, AdmissionDecision, FetchHandler, McpPromptDescriptor, McpResourceDescriptor, Middleware, MountFetchOptions, PromptArgument, PromptMessage, ResponseFinalization, RouteDescriptor, RunningServer, ServerOptions, StopHook, ToolAnnotations, } // A plugin operates over arbitrary Server shapes; `any` here is the standard framework escape hatch // (the precise threading happens at the `use` call site, which is generic over the *concrete* `this`). // biome-ignore lint/suspicious/noExplicitAny: plugins are generic over any Server's Registry/Context export type AnyServer = Server // Plugin definers + their types now live in `./plugin.ts`; re-exported here so `.use()` callers and // existing importers keep resolving them from the server module. export { type ContextPlugin, type DefinePluginResult, defineContextPlugin, defineIdentityPlugin, definePlugin, defineRouterPlugin, type NifraPlugin, type PluginTypeCollapsed, } from "./plugin.ts" export type { IdentityPlugin } const DEFAULT_MAX_BODY_BYTES = 1_000_000 const DEFAULT_DRAIN_MS = 10_000 const DRAIN_POLL_MS = 10 const STOP_HOOK_TIMEOUT_MS = 5_000 /** Same-origin check for a WebSocket handshake (CSWSH default). {@link isSameOriginRequest} is the one * owner, shared with the server-function mount in `@nifrajs/web` - the two used to answer differently * for the same request, so a browser that could open a socket was told its POST was cross-origin. */ const wsSameOrigin = isSameOriginRequest // `jsonError`, `urlPartsOf`, `pathnameOf` moved to `./http.ts` (a dependency-free leaf shared with the // opt-in request lanes); re-exported so existing importers keep resolving from here. export { pathnameOf, urlPartsOf } from "./http.ts" /** 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.ts" // Query-string + urlencoded-form parsing now lives in `./query.ts`; re-exported so existing // importers keep resolving `searchOf`/`queryObjectOf`/`QueryValue` from here. export { type QueryValue, queryObjectOf, searchOf } function hasReplacementParam(params: Record): boolean { for (const key in params) { if (params[key]!.includes("\uFFFD")) return true } return false } function normalizeMountPrefix(path: string): string { if (!path.startsWith("/") || path.includes("?") || path.includes("#")) { throw new TypeError("mountFetch path must be an absolute pathname without a query or hash") } const withoutWildcard = path.endsWith("/*") ? path.slice(0, -2) : path if (withoutWildcard.includes("*") || withoutWildcard.includes(":")) { throw new TypeError("mountFetch path must be a literal prefix, optionally ending in /*") } if (withoutWildcard.length === 0) return "/" return withoutWildcard.length > 1 && withoutWildcard.endsWith("/") ? withoutWildcard.slice(0, -1) : withoutWildcard } function underMountPrefix(pathname: string, prefix: string): boolean { return prefix === "/" || pathname === prefix || pathname.startsWith(`${prefix}/`) } function stripMountPrefix(request: Request, prefix: string): Request { if (prefix === "/") return request const url = new URL(request.url) const rest = url.pathname.slice(prefix.length) url.pathname = rest === "" ? "/" : rest return new Request(url.href, request) } /** `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[] } function responseSet(ctx: RawContext): CtxSet { return ctx[CONTEXT_SET]() ?? EMPTY_RESPONSE_CONTROLS } function isContextlessNoArgArrow(handler: (context: never) => unknown): boolean { if (handler.length !== 0) return false try { return CONTEXTLESS_ARROW.test(functionToString.call(handler)) } catch { return false } } /** `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 Web lanes' `wrapResponse` input, as a `Response`. A plain-data carrier ({@link plainError}, * `status(...)`) is rendered on the ordinary JSON lane rather than through its own `toResponse`, so * it picks up the same prebuilt init a handler's plain return uses. `EMPTY_RESPONSE_CONTROLS` because * these renders happen where no request context exists - before routing, or after it was abandoned. */ const webResponseOf = (result: Response | ResponseResult): Response => result instanceof Response ? result : toResponse(result, EMPTY_RESPONSE_CONTROLS) // Stable module-level finalizers so `fetch`/`resolveNode` allocate no per-request closures. const IDENTITY_RESPONSE = (response: Response | ResponseResult): Response => webResponseOf(response) const RESPONSE_TIMEOUT = (): Response => jsonError(503, "request_timeout") // The unused-order-scoped-hook audit (recorder, seal check, stack capture, message) is a dev-time // diagnostic. Every guard below keeps the INLINE literal `process.env.NODE_ENV !== "production"` at the // branch - never a shared const holding the folded boolean - because a bundler's `process.env.NODE_ENV` // define only dead-code-eliminates the audit when the literal appears at the `if`; Bun does not // propagate a module const's value into the branch. Every real production bundler (vite/esbuild/webpack/ // next, `bun build --production`) injects that define, turning each guard into `hookAuditRuntime && // false`, which folds away - dropping the whole audit and restoring the bare kernel size. // // `hookAuditRuntime` is the one shared flag: `typeof globalThis.Deno === "undefined"`. Deno ships a // node-compat `process`, so `typeof process` does NOT gate it out there, yet reading `process.env` // throws `NotCapable` without `--allow-env`. The flag short-circuits before the env read on Deno (audit // off) while a production define still folds ` && false` to `false` on every other runtime. const hookAuditRuntime = typeof (globalThis as { Deno?: unknown }).Deno === "undefined" // This module's own path, captured once from a sentinel stack. Every internal frame of an // order-scoped hook push (the public method, its `use()`-bundle dispatcher, the recorder) lives here, // so `captureCallerSite` skips anything starting with it and lands on the user's calling frame. const SERVER_MODULE_PATH = /* @__PURE__ */ ((): string | undefined => { const frame = new Error().stack?.split("\n")[1] return frame?.match(/\(?([^()\s]+):\d+:\d+\)?\s*$/)?.[1] })() /** One `file:line:col` frame for the caller of an order-scoped hook method, for the unused-hook * report. Only ever called behind the `unusedScopedHooks !== "off"` guard, so the `Error` (the sole * non-trivial cost) is never constructed when the check is disabled. */ const captureCallerSite = (): string | undefined => { const stack = new Error().stack if (stack === undefined) return undefined for (const line of stack.split("\n").slice(1)) { const frame = line.match(/\(?([^()\s]+:\d+:\d+)\)?\s*$/)?.[1] if (frame === undefined) continue if (SERVER_MODULE_PATH !== undefined && frame.startsWith(`${SERVER_MODULE_PATH}:`)) continue return frame } return undefined } /** Per-server bookkeeping for the unused-order-scoped-hook audit. Deliberately NOT `Server` fields or * methods: those emit class members (a field declaration, a method shell) that a bundler cannot remove * even after their bodies fold away. Held here in a module WeakMap and reached only through the * functions below, every call guarded by `process.env.NODE_ENV !== "production"`, so a production define * folds the guards away, leaves this map and these functions unreferenced, and the bundler eliminates * the whole audit - restoring the bare kernel size. */ interface HookAuditState { readonly policy: "warn" | "error" | "off" readonly hasCustomLogger: boolean readonly sites: { readonly kind: string readonly routesAtPush: number readonly site: string | undefined }[] checked: boolean } const hookAudit = /* @__PURE__ */ new WeakMap() /** Start tracking one server's order-scoped hook pushes (once, from the constructor). */ const beginHookAudit = ( key: object, policy: "warn" | "error" | "off", hasCustomLogger: boolean, ): void => { hookAudit.set(key, { policy, hasCustomLogger, sites: [], checked: false }) } /** Record one order-scoped hook push: the route count at that moment (so `routesAtPush === total` at * seal means no route was declared after it) and the caller's frame. */ const recordScopedHook = (key: object, routesAtPush: number, kind: string): void => { const state = hookAudit.get(key) if (state === undefined || state.policy === "off") return state.sites.push({ kind, routesAtPush, site: captureCallerSite() }) } /** Run the deadness check exactly once. A hook whose recorded route count equals the final route count * had no route declared after it - unambiguously dead, since the scoping feature (a hook that covers * only later routes) always leaves at least one such route. Zero-route apps are exempt (different * mistake, and the warning would be noise). */ const sealHookAudit = ( key: object, total: number, logger: { warn(message: string): void }, ): void => { const state = hookAudit.get(key) if (state === undefined || state.checked) return state.checked = true if (state.policy === "off" || total === 0) return const dead = state.sites.filter((hook) => hook.routesAtPush === total) if (dead.length === 0) return const detail = dead.map((hook) => ` ${hook.kind}() at ${hook.site ?? ""}`).join("\n") const message = `[nifra] ${dead.length} order-scoped hook(s) apply to no route - they were added after the ` + `last route was registered, and register() snapshots the chain into each route as it is ` + `declared:\n${detail}\nMove them before the routes they should cover. App-global hooks ` + `(onRequest/onResponse) are not order-scoped and can be added at any time.` if (state.policy === "error") throw new FrameworkError("UNUSED_SCOPED_HOOKS", message) if (state.hasCustomLogger) logger.warn(message) else console.warn(message) } /** * 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 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. */ declare readonly __nifraCoreVersion: NifraFeatureVersion private readonly catalog: RouteCatalog /** WebSocket routes, matched separately at upgrade time (a GET + `Upgrade: websocket`). */ private readonly wsRouter: Router private wsRouteCount: number /** 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: TopicRegistry | undefined /** 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 = false /** 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: | ((topic: string, data: string | ArrayBufferView | ArrayBuffer) => void) | undefined private readonly maxBodyBytes: number private readonly protoPoisoning: "reject" | "strip" | "ignore" /** `trustBodyFraming`: mark every `app.fetch` request as runtime-framed (see ServerOptions). */ private readonly trustBodyFraming: boolean private readonly wsMaxPayloadBytes: number private readonly requestTimeoutMs: number /** 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: ResponseContractRuntime | undefined /** Opt-in caller-IP trust declaration; `undefined` = socket peer only, no forwarded header believed. */ private readonly clientIpTrust: ClientIpTrust | undefined private readonly acceptInboundDeadlines: boolean private readonly maxInboundDeadlineMs: number private readonly deadlineAdmissionOptions: DeadlineAdmissionOptions private readonly gracefulSignals: boolean private readonly stopHooks: StopHook[] private readonly fetchMounts: FetchMount[] /** Capacity-admission gate; `undefined` = off (the request path pays nothing). */ private readonly capacityGate: AdmissionController | undefined private readonly onCapabilityUse: ((event: CapabilityUseEvent) => void) | undefined private readonly capabilityInterceptors: RegisteredCapabilityInterceptor[] private readonly capabilityObservers: EffectLifecycleObserver[] /** The installed effect-ledger runtime (owns the sink + per-route resolution + settle), or * `undefined` when the effect-ledger plugin is not installed. */ private effectLedgerRuntime: EffectLedgerRuntime | undefined /** The installed idempotency runtime (owns the app-wide default store + the dedupe lane), or * `undefined` when the idempotency plugin is not installed. */ private idempotencyRuntime: IdempotencyRuntime | undefined /** Installed opt-in runtime for `.tool()`/`.resource()`/`.prompt()`; `undefined` until `.use(mcp())`. */ private mcpRuntime: McpRuntime | undefined /** Installed Node-direct renderer for direct `resolveNode()` callers; `undefined` until `.use(nodeDirect())`. */ private nodeOutcomeRuntime: NodeOutcomeRuntime | undefined /** Installed streaming runtime for `.sse()` routes; `undefined` until `.use(streaming())`. */ private sseRuntime: SseRuntime | undefined /** Installed WebSocket runtime for `.ws()` routes; `undefined` until `.use(websocket())`. */ private wsRuntime: WsRuntime | undefined private readonly logger: Logger /** How much of an unhandled error {@link logRequestError} records. See `ServerOptions.errorLogDetail`. */ private readonly errorLogDetail: "full" | "message" | "none" /** App-wide validation-error fallback; a route's own `schema.onValidationError` takes precedence. */ private readonly defaultOnValidationError?: RouteSchema["onValidationError"] private bunServer: RunningServer | undefined private sealed: boolean private readonly derives: RawDerive[] private readonly decorations: Record private readonly beforeHandleHooks: RawBeforeHandle[] private readonly afterHandleHooks: RawAfterHandle[] private readonly onErrorHooks: RawErrorHandler[] private readonly aroundHooks: RawAround[] private readonly onRequestHooks: RawOnRequest[] private readonly onNodeRequestHooks: Array /** Registration-time eligibility for the Node request twin lane. */ private nodeRequestHooksComplete: boolean private readonly onResponseHooks: RawOnResponse[] private readonly onNodeResponseHooks: Array /** Registration-time eligibility for the Node response twin lane. */ private nodeResponseHooksComplete: boolean /** * 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: boolean private readonly onResponseFinalizedHooks: RawOnResponseFinalized[] /** * 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: StaticResponseHeaders | undefined /** `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: (response: Response | ResponseResult) => Response private webResponseTimeout: () => Response /** Body/raw response hooks need a framework-payload marker; keep that decision per app. */ private responseBodyTag: object | undefined private readonly responseBodyOwners: Set private readonly finalizeResponse = (result: unknown, set: CtxSet): Response => toResponse(result as HandlerResult, set, this.responseBodyTag, this.staticResponseHeaders) private readonly responseRequests: WeakMap /** Original source → request observed by generic request hooks, including in-place mutations. */ private readonly responseSources: WeakMap /** Memoized NodeRequestContext per plain-`Request` source - see {@link nodeRequestContextOf}. */ private readonly nodeContexts: WeakMap /** Names of plugins/middleware already applied via `use` - for idempotent dedupe. */ private readonly appliedPlugins: Set /** Order-scoped evidence captured by routes registered after an assured plugin. */ private readonly activeAssurance: AssuranceDeclaration[] /** App-wide evidence from global hooks; applies retroactively to every route. */ private readonly globalAssurance: AssuranceDeclaration[] /** App-declared MCP resources / prompts (via {@link resource} / {@link prompt}), read by `nifra mcp`. */ private readonly mcpResourceList: McpResourceDescriptor[] private readonly mcpPromptList: McpPromptDescriptor[] constructor(options: ServerOptions = {}) { this.catalog = new RouteCatalog() this.wsRouter = new Router() this.wsRouteCount = 0 this.topics = undefined const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES assertByteLimit(maxBodyBytes, "maxBodyBytes") this.protoPoisoning = options.protoPoisoning ?? "reject" this.trustBodyFraming = options.trustBodyFraming ?? false const wsMaxPayloadBytes = options.wsMaxPayloadBytes ?? maxBodyBytes assertByteLimit(wsMaxPayloadBytes, "wsMaxPayloadBytes") this.maxBodyBytes = maxBodyBytes this.wsMaxPayloadBytes = wsMaxPayloadBytes this.requestTimeoutMs = options.requestTimeoutMs ?? 0 this.clientIpTrust = options.clientIp this.acceptInboundDeadlines = options.acceptInboundDeadlines ?? false this.maxInboundDeadlineMs = options.maxInboundDeadlineMs ?? 30_000 this.deadlineAdmissionOptions = Object.freeze({ localTimeoutMs: this.requestTimeoutMs, maxInboundDeadlineMs: this.maxInboundDeadlineMs, }) if (!Number.isFinite(this.requestTimeoutMs) || this.requestTimeoutMs < 0) { throw new RangeError("requestTimeoutMs must be a finite non-negative number") } if (!Number.isFinite(this.maxInboundDeadlineMs) || this.maxInboundDeadlineMs <= 0) { throw new RangeError("maxInboundDeadlineMs must be a finite positive number") } this.gracefulSignals = options.gracefulSignals ?? false this.stopHooks = [] this.fetchMounts = [] this.capacityGate = options.admission this.onCapabilityUse = options.onCapabilityUse this.capabilityInterceptors = [] this.capabilityObservers = [] // The effect-ledger runtime is installed by `.use(effectLedger())`; a bare app never imports it, so // the ledger machinery tree-shakes out. Capability-declaring routes simply carry no ledger without it. this.effectLedgerRuntime = undefined // The idempotency runtime is installed by `.use(idempotency())`; a bare app never imports it, so the // dedupe machinery tree-shakes out. A route that declares idempotency without it is a build error. this.responseContractRuntime = undefined this.idempotencyRuntime = undefined this.logger = options.logger ?? jsonLogger() this.errorLogDetail = options.errorLogDetail ?? "full" this.defaultOnValidationError = options.onValidationError this.bunServer = undefined this.sealed = false // Guarded so a production define strips the call, leaving `beginHookAudit`/`hookAudit` unreferenced // for the bundler to eliminate; a non-bundled production run skips it the same way. if (hookAuditRuntime && process.env.NODE_ENV !== "production") { beginHookAudit(this, options.unusedScopedHooks ?? "warn", options.logger !== undefined) } this.derives = [] this.decorations = {} this.beforeHandleHooks = [] this.afterHandleHooks = [] this.onErrorHooks = [] this.aroundHooks = [] this.onRequestHooks = [] this.onNodeRequestHooks = [] this.nodeRequestHooksComplete = true this.onResponseHooks = [] this.onNodeResponseHooks = [] this.nodeResponseHooksComplete = true this.hasRawNodeResponseHook = false this.onResponseFinalizedHooks = [] this.staticResponseHeaders = undefined this.wrapWebResponse = IDENTITY_RESPONSE this.webResponseTimeout = RESPONSE_TIMEOUT this.responseBodyTag = undefined this.responseBodyOwners = new Set() this.responseRequests = new WeakMap() this.responseSources = new WeakMap() this.nodeContexts = new WeakMap() this.appliedPlugins = new Set() this.activeAssurance = [] this.globalAssurance = [] this.mcpResourceList = [] this.mcpPromptList = [] } private assertConfigurable(operation: string): void { if (this.sealed) { throw new FrameworkError( "SERVER_SEALED", `server configuration is sealed after listen(); call ${operation} before listen()`, ) } } /** Register a callback awaited after graceful drain and server stop. Hung callbacks are bounded. */ onStop(fn: StopHook): this { this.assertConfigurable("onStop()") if (typeof fn !== "function") throw new TypeError("onStop callback must be a function") this.stopHooks.push(fn) return 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 { this.assertConfigurable("mountFetch()") if (typeof handler !== "function") throw new TypeError("mountFetch handler must be a function") const mount: FetchMount = { path: normalizeMountPrefix(path), handler: handler as MountedFetchHandler, stripPrefix: options.stripPrefix === true, } this.fetchMounts.push(mount) this.fetchMounts.sort((a, b) => b.path.length - a.path.length) return 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< R, Ctx & ContextExtensionOf>, HookOutput | StatusResponseOf>> > { this.assertConfigurable("derive()") this.derives.push(fn as unknown as RawDerive) if (hookAuditRuntime && process.env.NODE_ENV !== "production") recordScopedHook(this, this.catalog.size, "derive") return this as unknown as Server< R, Ctx & ContextExtensionOf>, 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> { this.assertConfigurable("decorate()") this.decorations[key] = value if (hookAuditRuntime && process.env.NODE_ENV !== "production") recordScopedHook(this, this.catalog.size, "decorate") return this as unknown as 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 { this.assertConfigurable("onRequest()") this.onRequestHooks.push(fn as RawOnRequest) this.onNodeRequestHooks.push(undefined) this.nodeRequestHooksComplete = false return 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>> { this.assertConfigurable("beforeHandle()") this.beforeHandleHooks.push(fn as unknown as RawBeforeHandle) if (hookAuditRuntime && process.env.NODE_ENV !== "production") recordScopedHook(this, this.catalog.size, "beforeHandle") return this as unknown as 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< H extends (context: Context & Ctx, next: () => MaybePromise) => MaybePromise, >(fn: H): Server>> { this.assertConfigurable("around()") this.aroundHooks.push(fn as unknown as RawAround) if (hookAuditRuntime && process.env.NODE_ENV !== "production") recordScopedHook(this, this.catalog.size, "around") return this as unknown as 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 { this.assertConfigurable("aroundCapability()") if (typeof interceptor !== "function") { throw new TypeError("aroundCapability interceptor must be a function") } const timeoutMs = options.timeoutMs ?? DEFAULT_CAPABILITY_INTERCEPTOR_TIMEOUT_MS if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) { throw new RangeError("aroundCapability timeoutMs must be a positive safe integer") } // JS timers wrap larger delays to ~1ms, which would unexpectedly deny every effect. this.capabilityInterceptors.push( Object.freeze({ interceptor, timeoutMs: Math.min(timeoutMs, 2_147_483_647) }), ) if (hookAuditRuntime && process.env.NODE_ENV !== "production") recordScopedHook(this, this.catalog.size, "aroundCapability") return this } /** Observe token-only admission/execution lifecycle events for subsequent capability routes. */ observeCapability(observer: EffectLifecycleObserver): this { this.assertConfigurable("observeCapability()") if (typeof observer !== "function") { throw new TypeError("observeCapability observer must be a function") } this.capabilityObservers.push(observer) return 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>> { this.assertConfigurable("afterHandle()") this.afterHandleHooks.push(fn as unknown as RawAfterHandle) if (hookAuditRuntime && process.env.NODE_ENV !== "production") recordScopedHook(this, this.catalog.size, "afterHandle") return this as unknown as 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>> { this.assertConfigurable("onError()") this.onErrorHooks.push(fn as unknown as RawErrorHandler) if (hookAuditRuntime && process.env.NODE_ENV !== "production") recordScopedHook(this, this.catalog.size, "onError") return this as unknown as Server>> } /** Transform every outgoing response - success, error, 404, 405, short-circuit. Global. */ onResponse(fn: (response: Response, req: Request) => MaybePromise): this { this.assertConfigurable("onResponse()") this.onResponseHooks.push(fn) this.onNodeResponseHooks.push(undefined) this.nodeResponseHooksComplete = false return 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 { this.assertConfigurable("responseHeaders()") return this.addStaticResponseHeaders(normalizeStaticResponseHeaders(record)) } /** 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(record: Record): this { if (this.onResponseHooks.length > 0) { const observerHeaders = ( this as unknown as Pick ).onResponseHeaders if (observerHeaders !== undefined) { observerHeaders.call(this, (headers) => { for (const name of Object.keys(record)) { if (!headers.has(name)) headers.set(name, record[name] as string) } }) return this } // Route the hook through the same seam the static tier uses. On a framework response (headers // stamped mutable) it applies the defaults in place and returns the SAME object, so the // serialized-body marker survives for later `onResponseBody`/`onResponseRaw` observers and the // Node writer; reconstructing a `Response` here would strip that marker and reclassify the Node // outcome from `json` to a generic `response`. A guarded foreign response takes its clone path. const statics = buildStaticResponseHeaders(record) this.onResponseHooks.push((response) => applyStaticResponseHeaders(response, statics)) this.onNodeResponseHooks.push(undefined) this.nodeResponseHooksComplete = false return this } const merged = this.staticResponseHeaders === undefined ? record : { ...this.staticResponseHeaders.record, ...record } this.staticResponseHeaders = buildStaticResponseHeaders(merged) const statics = this.staticResponseHeaders this.wrapWebResponse = (response) => applyStaticResponseHeaders(webResponseOf(response), statics) this.webResponseTimeout = () => applyStaticResponseHeaders(RESPONSE_TIMEOUT(), statics) return this } private enableResponseBodyTagging(): object { if (this.responseBodyTag === undefined) { this.responseBodyTag = Object.freeze({}) this.responseBodyOwners.add(this.responseBodyTag) } return this.responseBodyTag } private responseObserverMethods(): ResponseObserverMethods { const methods = this as unknown as Partial if ( typeof methods.onResponseHeaders !== "function" || typeof methods.onResponseBody !== "function" || typeof methods.onResponseRaw !== "function" ) { throw new TypeError( "response observation requires `.use(responseObserver())` or an observer-enabled middleware", ) } return methods as ResponseObserverMethods } /** Observe the terminal response after all transformations. Observers are ordered and fail-open. */ onResponseFinalized( fn: (outcome: ResponseFinalization, req: Request) => MaybePromise, ): this { this.assertConfigurable("onResponseFinalized()") this.onResponseFinalizedHooks.push(fn) return 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 use(arg: Middleware | ((app: this) => AnyServer)): AnyServer { this.assertConfigurable("use()") if (typeof arg === "function") { const name = (arg as { pluginName?: string }).pluginName if (name !== undefined) { if (this.appliedPlugins.has(name)) return this // idempotent: already applied this.appliedPlugins.add(name) } const evidence = assuranceDeclarationsOf(arg) const pluginOnly = evidence.filter((item) => item.scope === "plugin") this.globalAssurance.push(...evidence.filter((item) => item.scope === "global")) this.activeAssurance.push(...evidence.filter((item) => item.scope === "subsequent")) this.activeAssurance.push(...pluginOnly) try { return arg(this) } finally { // Remove only this plugin's temporary evidence. Nested assured plugins may deliberately leave // subsequent evidence active, so truncating the whole array would lose real ordering semantics. for (const item of pluginOnly) { const index = this.activeAssurance.indexOf(item) if (index !== -1) this.activeAssurance.splice(index, 1) } } } if (arg.name !== undefined) { if (this.appliedPlugins.has(arg.name)) return this this.appliedPlugins.add(arg.name) } const evidence = assuranceDeclarationsOf(arg) if (evidence.some((item) => item.scope === "plugin")) { throw new Error('route assurance: scope "plugin" may only annotate a plugin function') } this.globalAssurance.push(...evidence.filter((item) => item.scope === "global")) this.activeAssurance.push(...evidence.filter((item) => item.scope === "subsequent")) const responseObserver = (arg as unknown as Record)[INSTALL_RESPONSE_OBSERVER] if (responseObserver !== undefined) { if ( typeof responseObserver !== "object" || responseObserver === null || typeof (responseObserver as { install?: unknown }).install !== "function" ) { throw new TypeError("response observer middleware has an invalid runtime") } this[INSTALL_RESPONSE_OBSERVER](responseObserver as ResponseObserverRuntime) } if (arg.onRequest !== undefined) { this.assertConfigurable("onRequest()") this.onRequestHooks.push(arg.onRequest as RawOnRequest) this.onNodeRequestHooks.push(arg.onNodeRequest) if (arg.onNodeRequest === undefined) this.nodeRequestHooksComplete = false } else if (arg.onNodeRequest !== undefined) { throw new TypeError("onNodeRequest() requires a paired onRequest() hook") } if (arg.around !== undefined) this.around(arg.around) if (arg.beforeHandle !== undefined) this.beforeHandle(arg.beforeHandle) if (arg.afterHandle !== undefined) this.afterHandle(arg.afterHandle) if (arg.onResponse !== undefined) { this.assertConfigurable("onResponse()") this.onResponseHooks.push(arg.onResponse) this.onNodeResponseHooks.push(arg.onNodeResponse) if (arg.onNodeResponse === undefined) this.nodeResponseHooksComplete = false else this.hasRawNodeResponseHook = true } else if (arg.onNodeResponse !== undefined) { throw new TypeError("onNodeResponse() requires a paired onResponse() hook") } // Before the bundle's own hooks: a bundle declaring both means its static values are the // defaults its hook may then override, which is the order a single bundle reads in. if (arg.responseHeaders !== undefined) this.responseHeaders(arg.responseHeaders) if (arg.onResponseHeaders !== undefined) this.responseObserverMethods().onResponseHeaders(arg.onResponseHeaders) if (arg.onResponseBody !== undefined) this.responseObserverMethods().onResponseBody(arg.onResponseBody) if (arg.onResponseRaw !== undefined) this.responseObserverMethods().onResponseRaw(arg.onResponseRaw) if (arg.onResponseFinalized !== undefined) this.onResponseFinalized(arg.onResponseFinalized) if (arg.onError !== undefined) this.onError(arg.onError) return this } get>( path: Path, schema: S, handler: H, ): Server< AddRoute, HookOutput>>, Ctx, HookOutput > get>( path: Path, handler: H, ): Server< AddRoute, OutputOf, HookOutput>>, Ctx, HookOutput > get( path: string, schemaOrHandler: RouteSchema | ErasedHandler, handler?: ErasedHandler, ): Server { return this.route("GET", path, schemaOrHandler, handler) } /** * 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< AddRoute>, Ctx, HookOutput > { const handler = (context: Context & Ctx): Response => requireSseRuntime(this.sseRuntime).response(context, (stream) => run(context, stream), init) return this.route("GET", path, schema, handler as unknown as ErasedHandler) as Server< AddRoute>, Ctx, HookOutput > } post>( path: Path, schema: S, handler: H, ): Server< AddRoute, HookOutput>>, Ctx, HookOutput > post>( path: Path, handler: H, ): Server< AddRoute, OutputOf, HookOutput>>, Ctx, HookOutput > post( path: string, schemaOrHandler: RouteSchema | ErasedHandler, handler?: ErasedHandler, ): Server { return this.route("POST", path, schemaOrHandler, handler) } put>( path: Path, schema: S, handler: H, ): Server< AddRoute, HookOutput>>, Ctx, HookOutput > put>( path: Path, handler: H, ): Server< AddRoute, OutputOf, HookOutput>>, Ctx, HookOutput > put( path: string, schemaOrHandler: RouteSchema | ErasedHandler, handler?: ErasedHandler, ): Server { return this.route("PUT", path, schemaOrHandler, handler) } patch>( path: Path, schema: S, handler: H, ): Server< AddRoute, HookOutput>>, Ctx, HookOutput > patch>( path: Path, handler: H, ): Server< AddRoute, OutputOf, HookOutput>>, Ctx, HookOutput > patch( path: string, schemaOrHandler: RouteSchema | ErasedHandler, handler?: ErasedHandler, ): Server { return this.route("PATCH", path, schemaOrHandler, handler) } delete>( path: Path, schema: S, handler: H, ): Server< AddRoute, HookOutput>>, Ctx, HookOutput > delete>( path: Path, handler: H, ): Server< AddRoute, OutputOf, HookOutput>>, Ctx, HookOutput > delete( path: string, schemaOrHandler: RouteSchema | ErasedHandler, handler?: ErasedHandler, ): Server { return this.route("DELETE", path, schemaOrHandler, handler) } /** * 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< Name extends string, S extends { description: string input: StandardSchemaV1 output?: StandardSchemaV1 annotations?: ToolAnnotations }, H extends ( input: InferOutput, ctx: Context & Ctx, ) => MaybePromise : unknown>, >( name: Name, config: S, handler: H, ): Server< AddRoute< R, "POST", `/_nifra/tool/${Name}`, RouteInfoFor< `/_nifra/tool/${Name}`, S["output"] extends StandardSchemaV1 ? { body: S["input"]; response: S["output"] } : { body: S["input"] }, OutputOf, HookOutput > >, Ctx, HookOutput > tool( name: string, config: { description: string input: StandardSchemaV1 output?: StandardSchemaV1 annotations?: ToolAnnotations }, handler: (input: unknown, ctx: Context & Ctx) => unknown, ): Server { const plan = requireMcpRuntime(this.mcpRuntime).tool( name, config, handler as (input: unknown, context: Context) => unknown, ) this.register("POST", plan.path, plan.schema, plan.run as (context: never) => unknown) // Tag the just-registered descriptor as an MCP tool. `tool` is readonly on RouteDescriptor (an // introspection field), so write it through a narrow mutable view - not `any`. const lastRoute = this.catalog.lastDescriptor() if (lastRoute) { ;(lastRoute as { tool?: RouteDescriptor["tool"] }).tool = plan.descriptor } return this as unknown as Server } /** * 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 { this.assertConfigurable("resource()") this.mcpResourceList.push(requireMcpRuntime(this.mcpRuntime).resource(uri, config, read)) return this } /** * 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 { this.assertConfigurable("prompt()") this.mcpPromptList.push(requireMcpRuntime(this.mcpRuntime).prompt(name, config, handler)) return this } /** The MCP resources declared via {@link resource} - enumerated by `nifra mcp`. */ mcpResources(): readonly McpResourceDescriptor[] { return this.mcpResourceList } /** The MCP prompts declared via {@link prompt} - enumerated by `nifra mcp`. */ mcpPrompts(): readonly McpPromptDescriptor[] { return this.mcpPromptList } /** * 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< Data = unknown, Schema extends StandardSchemaV1 | undefined = undefined, Send extends StandardSchemaV1 | undefined = undefined, Path extends string = string, >( path: Path, handler: WebSocketHandler, Schema, Send>, ): string extends Path ? Server : Server>, Ctx, HookOutput> { this.assertConfigurable("ws()") // Boot-time guard: the WS runtime is a subpath (`@nifrajs/core/ws`) so no-WebSocket apps don't // bundle it. Registration is the loud, early failure point - never the first connection. const runtime = requireWsRuntime(this.wsRuntime) if (handler.validateSend === true && handler.sendSchema === undefined) { throw new RouteConfigError( "INVALID_WS_SEND_VALIDATION", `route WS ${path}: validateSend requires sendSchema`, ) } // A validated-send route makes broadcast bytes route-dependent (drop-on-invalid per socket), so the // whole app forgoes native pub/sub and keeps the JS registry loop that runs each send through the sender. if (handler.validateSend === true) this.wsHasValidatedSend = true this.topics ??= runtime.createTopics() // A `messageSchema` wraps `message` with validation once, here - every adapter then dispatches // already-validated, typed messages (Bun/Deno/Node/Workers) with no per-adapter code. this.wsRouter.add("GET", path, { handler: runtime.wrapHandler(handler as WebSocketHandler), }) this.wsRouteCount += 1 return this as never } /** * 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 { // Native-pubsub mode (Bun, no validated-send route): one native broadcast to every subscriber, no // per-connection JS loop. Bound only after `listen()`; before that (or off Bun) the registry runs. if (this.nativePublish !== undefined) { this.nativePublish(topic, data) return } // No `app.ws()` yet ⇒ no registry and necessarily no subscribers - a publish is a no-op anyway. this.topics?.publish(topic, data) } private route( method: Method, path: string, schemaOrHandler: RouteSchema | ErasedHandler, handler?: ErasedHandler, ): Server { let rawHandler: ErasedHandler let schema: RouteSchema | undefined if (handler !== undefined) { schema = schemaOrHandler as RouteSchema rawHandler = handler } else { schema = undefined rawHandler = schemaOrHandler as ErasedHandler } this.register(method, path, schema, rawHandler) // The accumulated registry type is compile-time only; the same instance // carries every route, so the public methods re-type `this` per call. return this as unknown as Server } /** * 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 { this.assertConfigurable("route registration") this.catalog.add(this.prepareRoute(method, path, schema, handler)) } /** 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 { this.assertConfigurable("route registration") const staged = routes.map(({ method, path, schema, handler }) => this.prepareRoute(method, path, schema, handler), ) this.catalog.addBatch(staged) } private prepareRoute( method: Method, path: string, schema: RouteSchema | undefined, handler: (context: never) => unknown, ): CatalogRoute { const compiled = compileRouteOptions( { maxBodyBytes: this.maxBodyBytes, activeAssurance: this.activeAssurance, globalAssurance: this.globalAssurance, decorations: this.decorations, onCapabilityUse: this.onCapabilityUse, capabilityInterceptors: this.capabilityInterceptors, capabilityObservers: this.capabilityObservers, effectLedgerRuntime: this.effectLedgerRuntime, idempotencyRuntime: this.idempotencyRuntime, responseContractRuntime: this.responseContractRuntime, derives: this.derives, beforeHandleHooks: this.beforeHandleHooks, afterHandleHooks: this.afterHandleHooks, onErrorHooks: this.onErrorHooks, aroundHooks: this.aroundHooks, defaultOnValidationError: this.defaultOnValidationError, }, method, path, schema, handler, ) const { pattern, bodyLimit, capabilities, routeDecorations, hasDecorations, idempotent, ledgered, responseContract, lanes, routeAssurance, } = compiled const { bare, fusedQuery, fusedBody } = lanes const fusedBodyRunner = fusedBody ? this.buildFusedBodyRunner( handler as unknown as InternalHandler, schema?.body as StandardSchemaV1, hasDecorations ? routeDecorations : undefined, bodyLimit ?? UNLIMITED_BODY_BYTES, ) : undefined // Fused lifecycle lanes: derive + before (with or without an after), and body + the same shape. // The lane selectors in `selectRouteLanes` are exhaustive about the lifecycleHookLane / // body-derive-before-after eligibility, so we only need to check the lane and the hook counts. const lifecycleHookLane = lanes.lifecycleHookLane const isFusedLifecycle = lifecycleHookLane !== undefined && this.derives.length === 1 const isFusedBodyLifecycle = lanes.fusedLane === "body-derive-before" || lanes.fusedLane === "body-derive-before-after" const fusedWeb = bare && this.aroundHooks.length === 0 ? this.buildFusedWeb( handler as unknown as InternalHandler, hasDecorations ? routeDecorations : undefined, isContextlessNoArgArrow(handler), bodyLimit ?? UNLIMITED_BODY_BYTES, ) : fusedQuery ? this.buildFusedQueryWeb( handler as unknown as InternalHandler, hasDecorations ? routeDecorations : undefined, schema?.query as StandardSchemaV1, bodyLimit ?? UNLIMITED_BODY_BYTES, ) : fusedBody ? this.buildFusedBodyWeb(fusedBodyRunner as FusedBodyRunner) : isFusedBodyLifecycle ? this.buildFusedBodyDeriveBeforeAfter( handler as unknown as InternalHandler, this.derives[0]!, this.beforeHandleHooks[0]!, this.afterHandleHooks[0], schema?.body as StandardSchemaV1, bodyLimit ?? UNLIMITED_BODY_BYTES, ) : isFusedLifecycle && lifecycleHookLane === "derive-before" ? this.buildFusedDeriveBefore( handler as unknown as InternalHandler, this.derives[0]!, this.beforeHandleHooks[0]!, schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES, ) : isFusedLifecycle && lifecycleHookLane === "derive-before-after" ? this.buildFusedDeriveBeforeAfter( handler as unknown as InternalHandler, this.derives[0]!, this.beforeHandleHooks[0]!, this.afterHandleHooks[0]!, schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES, ) : undefined const program = compileRouteProgram({ schema, handler: handler as unknown as InternalHandler, derives: this.derives, beforeHandle: this.beforeHandleHooks, afterHandle: this.afterHandleHooks, onError: this.onErrorHooks, decorations: routeDecorations, hasDecorations, bodySchema: schema?.body, bodyLimit, responseContract, }) const contextless = lanes.bare && this.aroundHooks.length === 0 && isContextlessNoArgArrow(handler) const execution = compileRouteExecutionPlan({ lane: lanes.lane, contextless, hasAround: this.aroundHooks.length > 0, hasLedger: ledgered !== undefined, fusedWeb, fusedBody: fusedBodyRunner, fusedLane: lanes.fusedLane, }) const registeredEntry: RouteEntry = { handler: handler as unknown as InternalHandler, schema, bodyLimit, idempotent, ledgered, responseContract, derives: [...this.derives], decorations: routeDecorations, hasDecorations, beforeHandle: [...this.beforeHandleHooks], afterHandle: [...this.afterHandleHooks], onError: [...this.onErrorHooks], lifecycleHookLane: lanes.lifecycleHookLane, around: [...this.aroundHooks], execution, program, } const descriptor: RouteDescriptor = { method, path, schema, ...(capabilities.length > 0 ? { capabilities } : {}), ...(schema?.family === true ? { family: true } : {}), } return { method, path, pattern, entry: registeredEntry, descriptor, assurance: routeAssurance, } } /** 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( buffered: RequestSource, platform: Platform | undefined, entry: RouteEntry, params: Record, search: string | undefined, ): Promise { return Promise.resolve( this.runMatchedLanes( buffered, platform, entry, params, search, this.finalizeResponse, this.wrapWebResponse, this.webResponseTimeout, false, ), ) } /** @internal Symbol-keyed install seam for the `responseContract()` plugin. Off the public typed surface. */ [INSTALL_RESPONSE_CONTRACT](runtime: ResponseContractRuntime): void { this.assertConfigurable("responseContract()") this.responseContractRuntime = runtime } /** @internal Symbol-keyed install seam for the response observer plugin. */ [INSTALL_RESPONSE_OBSERVER](runtime: ResponseObserverRuntime): ResponseObserverMethods { this.assertConfigurable("responseObserver()") const methods = runtime.install({ assertConfigurable: (operation) => this.assertConfigurable(operation), addResponseHook: (web, node) => { this.onResponseHooks.push(web) this.onNodeResponseHooks.push(node) if (node === undefined) this.nodeResponseHooksComplete = false }, enableResponseBodyTagging: () => { if (this.responseBodyTag === undefined) { this.responseBodyTag = Object.freeze({}) this.responseBodyOwners.add(this.responseBodyTag) } return this.responseBodyTag }, responseBodyOwners: () => this.responseBodyOwners, }) Object.assign(this, methods) return methods } /** @internal Symbol-keyed install seam for the `idempotency()` plugin. Off the public typed surface. */ [INSTALL_IDEMPOTENCY](runtime: IdempotencyRuntime): void { this.assertConfigurable("idempotency()") this.idempotencyRuntime = runtime } /** @internal Symbol-keyed install seam for the `mcp()` plugin. Off the public typed surface. */ [INSTALL_MCP](runtime: McpRuntime): void { this.assertConfigurable("mcp()") this.mcpRuntime = runtime } /** @internal Symbol-keyed install seam for the `nodeDirect()` plugin. Off the public typed surface. */ [INSTALL_NODE_DIRECT](runtime: NodeOutcomeRuntime): void { this.assertConfigurable("nodeDirect()") this.nodeOutcomeRuntime = runtime } /** @internal Symbol-keyed install seam for the `streaming()` plugin. Off the public typed surface. */ [INSTALL_SSE](runtime: SseRuntime): void { this.assertConfigurable("streaming()") this.sseRuntime = runtime } /** @internal Symbol-keyed install seam for the `websocket()` plugin. Off the public typed surface. */ [INSTALL_WS](runtime: WsRuntime): void { this.assertConfigurable("websocket()") this.wsRuntime = runtime } /** * 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 { this.assertConfigurable("merge()") const source = other as unknown as Server if (source.wsRouteCount > 0) { throw new RouteConfigError( "INVALID_PATH", "merge() does not carry WebSocket routes - register .ws() routes on the parent server", ) } const sourceRoutes = source.catalog.entries() // Hooks and global assurance stay LOCAL to a group that has routes; a route-less group is a // middleware bundle whose hooks can only mean app-wide intent, so it keeps the global append. const scoped = sourceRoutes.length > 0 // A group's global assurance rides its (route-scoped) hooks: folded into each merged route's // own evidence rather than the parent's global list, so `routes()` never claims the group's // enforcement for parent routes the group's hooks do not see. const foldedAssurance = scoped && source.globalAssurance.length > 0 ? (route: CatalogRoute): CatalogRoute => ({ ...route, assurance: [...route.assurance, ...source.globalAssurance], }) : (route: CatalogRoute): CatalogRoute => route this.catalog.addBatch( sourceRoutes.map((route) => this.bindFusedRuntime(foldedAssurance(route))), ) // Resolved idempotency/ledger route entries carry their own store/sink configuration, while the // runtime object supplies the generic execution machinery. Preserve a group's installed runtime // when the parent has none so merging cannot silently disable a safety lane. If the parent already // has a runtime, either implementation can execute every resolved entry because route-specific // options were pinned during registration. this.responseContractRuntime ??= source.responseContractRuntime this.idempotencyRuntime ??= source.idempotencyRuntime this.effectLedgerRuntime ??= source.effectLedgerRuntime this.mcpRuntime ??= source.mcpRuntime this.nodeOutcomeRuntime ??= source.nodeOutcomeRuntime this.sseRuntime ??= source.sseRuntime this.wsRuntime ??= source.wsRuntime if (scoped && source.onRequestHooks.length > 0) { // Snapshot the group's routes into a dedicated matcher: the guard must reflect what was // MERGED, not whatever the group's own catalog grows into afterwards. One probe against it // gates each group hook to requests the group would serve; everything else passes untouched. const scope = new RouteCatalog() scope.addBatch(sourceRoutes) this.onRequestHooks.push( ...source.onRequestHooks.map( (hook): RawOnRequest => (req, platform) => scope.find(req.method, pathnameOf(req.url)).found ? hook(req, platform) : undefined, ), ) // An `undefined` slot marks an unpaired hook (position-aligned with `onRequestHooks`) and // must stay `undefined` - wrapping it would fabricate a Node twin that never existed. this.onNodeRequestHooks.push( ...source.onNodeRequestHooks.map((hook): NodeRequestHook | undefined => hook === undefined ? undefined : (req, platform) => scope.find(req.method, pathnameOf(req.url)).found ? hook(req, platform) : undefined, ), ) } else { this.onRequestHooks.push(...source.onRequestHooks) this.onNodeRequestHooks.push(...source.onNodeRequestHooks) } this.nodeRequestHooksComplete &&= source.nodeRequestHooksComplete // The group's static declarations came before its own response hooks, so they are folded in // first - and fold themselves into a hook here if this server already has one (same ordering // rule as a direct `responseHeaders()` call). if (source.staticResponseHeaders !== undefined) { this.addStaticResponseHeaders({ ...source.staticResponseHeaders.record }) } this.onResponseHooks.push(...source.onResponseHooks) this.onNodeResponseHooks.push(...source.onNodeResponseHooks) this.nodeResponseHooksComplete &&= source.nodeResponseHooksComplete this.hasRawNodeResponseHook ||= source.hasRawNodeResponseHook this.onResponseFinalizedHooks.push(...source.onResponseFinalizedHooks) if (source.responseBodyTag !== undefined) { const owner = this.enableResponseBodyTagging() this.responseBodyOwners.add(source.responseBodyTag) source.responseBodyOwners.add(owner) } if (!scoped) this.globalAssurance.push(...source.globalAssurance) this.mcpResourceList.push(...source.mcpResourceList) this.mcpPromptList.push(...source.mcpPromptList) return this as unknown as 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(route: CatalogRoute): CatalogRoute { const { entry } = route if (entry.execution.fusedWeb === undefined) return route // Rebuild with the SAME builder that produced the closure - rebinding a query-fused route as // bare would silently drop its validation. const lane = entry.execution.fusedLane // Type-erase the lane so each `===` branch sees the full union, not the narrowed remainder. const laneName: string | undefined = lane // Body+lifecycle routes have a fused Web renderer, but no body-only Node renderer: the latter // would bypass derive/before/after because the Node-direct dispatcher prefers `fusedBody` over // the generic execution plan. Only the original body-only lane may populate this slot. const fusedBody = laneName === "body" ? this.buildFusedBodyRunner( entry.handler, entry.schema?.body as StandardSchemaV1, entry.hasDecorations ? entry.decorations : undefined, entry.bodyLimit ?? UNLIMITED_BODY_BYTES, ) : undefined const derive = entry.derives[0] const before = entry.beforeHandle[0] const after = entry.afterHandle[0] const fusedWeb = laneName === "body" ? this.buildFusedBodyWeb(fusedBody as FusedBodyRunner) : laneName === "query" ? this.buildFusedQueryWeb( entry.handler, entry.hasDecorations ? entry.decorations : undefined, entry.schema?.query as StandardSchemaV1, entry.bodyLimit ?? UNLIMITED_BODY_BYTES, ) : laneName === "body-derive-before" && derive !== undefined && before !== undefined ? this.buildFusedBodyDeriveBeforeAfter( entry.handler, derive, before, undefined, entry.schema?.body as StandardSchemaV1, entry.bodyLimit ?? UNLIMITED_BODY_BYTES, ) : laneName === "body-derive-before-after" && derive !== undefined && before !== undefined && after !== undefined ? this.buildFusedBodyDeriveBeforeAfter( entry.handler, derive, before, after, entry.schema?.body as StandardSchemaV1, entry.bodyLimit ?? UNLIMITED_BODY_BYTES, ) : laneName === "derive-before" && derive !== undefined && before !== undefined ? this.buildFusedDeriveBefore( entry.handler, derive, before, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES, ) : laneName === "derive-before-after" && derive !== undefined && before !== undefined && after !== undefined ? this.buildFusedDeriveBeforeAfter( entry.handler, derive, before, after, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES, ) : this.buildFusedWeb( entry.handler, entry.hasDecorations ? entry.decorations : undefined, isContextlessNoArgArrow(entry.handler), entry.bodyLimit ?? UNLIMITED_BODY_BYTES, ) return { ...route, entry: { ...entry, execution: Object.freeze({ ...entry.execution, fusedWeb, fusedBody }), }, } } /** * 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 { if (this.activeAssurance.length === 0 && this.globalAssurance.length === 0) { if (!this.catalog.hasAssurance()) { return this.catalog.routeDescriptors() } } return this.catalog.entries().map(({ method, path, descriptor, assurance }) => { const effective = assuranceEvidenceFor([...assurance, ...this.globalAssurance], method, path) return effective.length > 0 ? { ...descriptor, assurance: effective } : descriptor }) } /** * 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 { // An edge deployment whose only ingress is this method declares its requests runtime-framed at // construction; everything else stays on the delivered-byte check (see `trustBodyFraming`). if (this.trustBodyFraming) markTrustedBodyFraming(req) // A real `Request` satisfies `RequestSource`, so it's passed straight through - no per-request // wrapper allocation on the Web/Bun hot path. return this.fetchSource(req, platform) } private fetchSource( source: RequestSource, platform?: Platform>, ): MaybePromise { // Seal once on the first request so test servers (no `listen()`) still get the dead-hook check. // After that it is a single boolean read - the per-request cost the option promises to keep at zero. if (hookAuditRuntime && process.env.NODE_ENV !== "production") sealHookAudit(this, this.catalog.size, this.logger) // Off path (default): straight through - one property check, no closure, no promise. if (this.capacityGate === undefined) return this.fetchSourceInner(source, platform) return this.admitGated(requestOf(source), () => this.fetchSourceInner(source, platform)) } private fetchSourceInner( source: RequestSource, platform?: Platform>, ): MaybePromise { // Non-`async` on purpose: `dispatch` may return a `Response` *synchronously* (the bare-route fast // path, selected by the compiled execution plan), and an `async fetch` would wrap every such result in a redundant // promise + microtask. Returning `Response | Promise` matches Web/edge handlers, while // `await app.fetch(...)` continues to work exactly as before. const outcome = this.dispatch( source, platform, this.finalizeResponse, this.wrapWebResponse, this.webResponseTimeout, true, ) if (this.onResponseHooks.length === 0 && this.onResponseFinalizedHooks.length === 0) { return outcome } // onResponse sees every response - success, validation error, 404/405, timeout, onRequest // short-circuit; normalize to a promise, then thread through the hooks. return outcome instanceof Promise ? outcome.then((response) => this.applyOnResponseAndFinalize(response, this.takeResponseRequest(source)), ) : this.applyOnResponseAndFinalize(outcome, this.takeResponseRequest(source)) } /** 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( source: RequestSource, entry: RouteEntry, params: Record, ): MaybePromise { if (this.capacityGate === undefined) return this.fetchMatchedInner(source, entry, params) return this.admitGated(requestOf(source), () => this.fetchMatchedInner(source, entry, params)) } private fetchMatchedInner( source: RequestSource, entry: RouteEntry, params: Record, ): MaybePromise { const outcome = this.runMatched( source, undefined, entry, params, undefined, this.finalizeResponse, this.wrapWebResponse, this.webResponseTimeout, true, ) if (this.onResponseHooks.length === 0 && this.onResponseFinalizedHooks.length === 0) { return outcome } return outcome instanceof Promise ? outcome.then((response) => this.applyOnResponseAndFinalize(response, requestOf(source))) : this.applyOnResponseAndFinalize(outcome, requestOf(source)) } /** * 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(req: Request, produce: () => MaybePromise): MaybePromise { const decision = (this.capacityGate as AdmissionController).admit(req) return decision instanceof Promise ? decision.then((settled) => this.runAdmitted(settled, produce)) : this.runAdmitted(decision, produce) } private runAdmitted( decision: AdmissionDecision, produce: () => MaybePromise, ): MaybePromise { if (!decision.admitted) return decision.response // shed: ready 429, no slot held let released = false const release = (): void => { if (released) return released = true decision.release() } let outcome: MaybePromise try { outcome = produce() } catch (error) { release() throw error } // Release the slot once the response settles - on resolve OR reject - via `finally`, which passes // the value/rejection through unchanged. (A single settle hook, rather than separate then-arms: the // request pipeline resolves handler errors to a Response, so a rejection arm would be unreachable.) if (outcome instanceof Promise) return outcome.finally(release) release() return outcome } /** * 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 { if ( this.onRequestHooks.length > 0 || this.onResponseHooks.length > 0 || this.onResponseFinalizedHooks.length > 0 || this.capacityGate !== undefined || this.staticResponseHeaders !== undefined || this.requestTimeoutMs !== 0 || this.acceptInboundDeadlines || this.clientIpTrust !== undefined ) { return undefined } const parts = source.urlParts ?? urlPartsOf(source.url) const match = this.catalog.find(source.method, parts.pathname) if (match.found) return undefined for (const mount of this.fetchMounts) { if (!underMountPrefix(parts.pathname, mount.path)) continue const candidate = (mount.handler as unknown as Record)[NODE_NATIVE_MOUNT] return typeof candidate === "function" ? { handler: candidate as NativeMountHandler, path: mount.path, stripPrefix: mount.stripPrefix, } : undefined } return 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 { return this.resolveNodeSource(req, platform) } resolveNodeSource( source: RequestSource, platform?: Platform>, suppliedRuntime?: NodeOutcomeRuntime, ): MaybePromise { // Seal once on the Node-direct lane too - it can bypass `fetchSource` entirely (hookless outcome). if (hookAuditRuntime && process.env.NODE_ENV !== "production") sealHookAudit(this, this.catalog.size, this.logger) // A paired header-only native hook can preserve the Node-direct JSON/body outcome for successful // responses. Arbitrary onResponse transforms need a real Web Response, but a buffered outcome can // be materialized with a direct-write marker and return to the socket path when the hook mutates // it in place. Finalization observers and the capacity gate still wrap the complete Web path. // The native response lane engages only when the REQUEST side is native too (or there are no // request hooks at all). This is what makes the NodeRequestContext identity contract hold: a // web request-hook walk can rewrite the request, so its response-side view is a synthetic // wrapper - a different object - and any middleware carrying per-request state from its request // twin to its response twin through a WeakMap would silently miss. Coupling the gates means a // response twin always sees the exact object its request twin saw. const nativeResponseHooks = this.canUseNodeResponseHooks() && (this.onRequestHooks.length === 0 || this.canUseNodeRequestHooks()) const webResponseHooks = this.onResponseHooks.length > 0 && !nativeResponseHooks if (this.onResponseFinalizedHooks.length > 0 || this.capacityGate !== undefined) { const response = this.fetchSource(source, platform) return response instanceof Promise ? response.then((settled) => ({ kind: "response", response: settled })) : { kind: "response", response } } // May resolve **synchronously** for a compiled bare route + sync handler - the `@nifrajs/node` // adapter `await`s the result, so it transparently handles either; the sync case allocates no promise // at all on the Node hot path. const runtime = suppliedRuntime ?? this.nodeOutcomeRuntime if (runtime === undefined) { throw new FrameworkError( "NODE_DIRECT_RUNTIME_MISSING", "resolveNode() needs the Node-direct renderer. Normal @nifrajs/node serving installs it automatically; direct callers should add `.use(nodeDirect())` (from `@nifrajs/core/node-direct`).", ) } const resolved = this.dispatch( source, platform, runtime.toOutcome, runtime.fromResponse, runtime.timeout, false, ) // Fold declared static headers into the record ONCE, here: before any native twin runs (so a // header or body hook reads them through its view), and on the no-hook path too (which returns // the outcome straight to the writer without the finish step below). const statics = this.staticResponseHeaders // The fold also publishes the all-lowercase proof, which is why it is worth doing here rather // than in the finish step: it is the only stage the HOOKLESS lane passes through, and that lane // returns straight to the writer below without ever reaching `finishNodeResponse`. const markLowercase = !this.hasRawNodeResponseHook const outcome = statics === undefined ? resolved : resolved instanceof Promise ? resolved.then((settled) => withStaticNodeHeaders(settled, statics, markLowercase)) : withStaticNodeHeaders(resolved, statics, markLowercase) if (webResponseHooks) { return outcome instanceof Promise ? outcome.then((settled) => this.finishNodeWebResponse(settled, source, runtime)) : this.finishNodeWebResponse(outcome, source, runtime) } if (!nativeResponseHooks) return outcome try { return outcome instanceof Promise ? outcome.then((settled) => this.finishNodeResponse(settled, source, runtime)) : this.finishNodeResponse(outcome, source, runtime) } catch (error) { // Keep resolveNode's failure shape promise-based, matching app.fetch and the adapter bridge. return Promise.reject(error) } } /** Run generic Web response middleware while retaining direct writes for untouched buffered bodies. */ private finishNodeWebResponse( outcome: NodeServeOutcome, source: RequestSource, runtime: NodeOutcomeRuntime, ): MaybePromise { const req = this.takeResponseRequest(source) const response = runtime.toResponse(outcome) const transformed = this.applyOnResponseAndFinalize(response, req) return transformed instanceof Promise ? transformed.then(runtime.fromResponse) : runtime.fromResponse(transformed) } /** True only when every transforming Web response hook has a header-only Node equivalent. */ private canUseNodeResponseHooks(): boolean { return this.onResponseHooks.length > 0 && this.nodeResponseHooksComplete } /** Apply paired native hooks to data outcomes; preserve the complete Web hook pipeline for Response outcomes. */ private finishNodeResponse( outcome: NodeServeOutcome, source: RequestSource, runtime: NodeOutcomeRuntime, ): MaybePromise { if (outcome.kind === "response") { const req = this.takeResponseRequest(source) const transformed = this.applyOnResponseAndFinalize(outcome.response, req) return transformed instanceof Promise ? transformed.then(runtime.fromResponse) : runtime.fromResponse(transformed) } let headers = outcome.headers as Record | undefined // ONE pass over the names, here, before any twin runs. The same question is asked three times per // request downstream - the Content-Type lookup just below, the header view's alias index, and the // `@nifrajs/node` writer's normalization gate - and each used to walk the record itself. An app // declaring static headers has already had it answered by the fold in `resolveNode`, for free, so // the pass runs only for the records nothing looked at yet. const lowercaseKeys = headers === undefined || hasLowercaseHeaderKeysMark(headers) || headerKeysAllLowercase(headers) if (outcome.kind === "json" && outcome.body !== null) { // The json render adds its Content-Type at WRITE time, so a body hook checking content types // would see nothing. Materialize the writer's own value into the hook-visible record - same // string the writer would emit, so the wire is unchanged. const hasType = headers !== undefined && (lowercaseKeys ? headers["content-type"] !== undefined : Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) if (!hasType) { const defaultContentType = runtime.jsonContentType ?? "application/json;charset=utf-8" if (headers === undefined) { headers = { "content-type": defaultContentType } } else { // The record belongs to this outcome. Add the implicit JSON type in place so the native // header walk does not clone every c.set.headers record before it can run. A hook that // replaces or deletes the type still takes the existing withNodeResponseHeaders path; // the writer's final defaulting behavior remains unchanged. headers["content-type"] = defaultContentType } } } // Publish the proof for the view and the direct writer - but only for an app whose every twin // normalizes case. A raw `onNodeResponse` twin writes the record straight, past the view, so a // mark set before it ran could be made to lie, and a lying mark would ship a mixed-case name that // `Headers` lowercases on every other runtime. Those apps keep the per-reader scans. if (lowercaseKeys && headers !== undefined && !this.hasRawNodeResponseHook) { markLowercaseHeaderKeys(headers) } const context: NodeResponseContext = { status: outcome.status, headers, cookies: outcome.kind === "json" ? outcome.cookies : undefined, body: outcome.body, } const applied = this.applyNodeResponseHooks(context, this.takeNodeResponseRequest(source)) if (applied instanceof Promise) { return applied.then(() => this.withNodeResponseHeaders(outcome, context)) } return this.withNodeResponseHeaders(outcome, context) } private withNodeResponseHeaders( outcome: Exclude, context: NodeResponseContext, ): NodeServeOutcome { const outcomeCookies = outcome.kind === "json" ? outcome.cookies : undefined const bodyChanged = context.body !== outcome.body const statusChanged = context.status !== outcome.status if ( context.headers === outcome.headers && context.cookies === outcomeCookies && !bodyChanged && !statusChanged ) { return outcome } let headers = context.headers if (bodyChanged && headers !== undefined) { // A replaced body invalidates any explicitly carried length; the writers re-derive framing // from the final bytes. const stale = Object.keys(headers).find((key) => key.toLowerCase() === "content-length") if (stale !== undefined) { headers = { ...headers } delete headers[stale] } } if (outcome.kind === "json") { if (bodyChanged && context.body !== null && typeof context.body !== "string") { // A binary replacement can't ride the json render - switch to the buffered-body render, // folding queued cookies into explicit set-cookie lines so nothing is dropped. const record = Object.create(null) as Record if (headers !== undefined) Object.assign(record, headers) if (context.cookies !== undefined && context.cookies.length > 0) { record["set-cookie"] = [...context.cookies] } return { kind: "body", status: context.status, headers: record, body: context.body } } return { ...outcome, status: context.status, headers, cookies: context.cookies, body: bodyChanged ? (context.body as string | null) : outcome.body, } } return { ...outcome, status: context.status, headers, body: bodyChanged ? ((context.body ?? new Uint8Array(0)) as string | Uint8Array) : outcome.body, } } /** Synchronous until a native response hook actually returns a Promise. */ private applyNodeResponseHooks( response: NodeResponseContext, req: NodeRequestContext, ): MaybePromise { for (let i = 0; i < this.onNodeResponseHooks.length; i++) { const hook = this.onNodeResponseHooks[i] as NodeResponseHook const result = hook(response, req) if (result instanceof Promise) return result.then(() => this.continueNodeResponseHooks(i + 1, response, req)) } } private async continueNodeResponseHooks( start: number, response: NodeResponseContext, req: NodeRequestContext, ): Promise { for (let i = start; i < this.onNodeResponseHooks.length; i++) { const hook = this.onNodeResponseHooks[i] as NodeResponseHook await hook(response, req) } } /** * 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 { if (this.wsRouteCount === 0) return WS_PASS if (req.headers.get("upgrade")?.toLowerCase() !== "websocket") return WS_PASS const url = urlPartsOf(req.url) const match = this.wsRouter.find("GET", url.pathname) if (!match.found) return WS_PASS // upgrade header, no WS route here → normal routing decides // Inspect only captured values for escapes. Scanning the full pathname repeated work the router // already did and made every plain dynamic route pay for unrelated static path bytes. const params = match.params === EMPTY_PARAMS ? match.params : decodeRouteParams(match.params) if (params === null) return { kind: "reject", response: jsonError(400, "malformed_path") } const handler = match.payload.handler // Non-null: wsRouteCount > 0 ⇒ ws() ran ⇒ `.use(websocket())` installed the runtime + registry. const pubsub = this.topics as TopicRegistry const attach = (this.wsRuntime as WsRuntime).attach // CSWSH guard, before any per-connection work or the user's upgrade(): reject a disallowed // Origin with 403. Browsers don't CORS-protect WS handshakes but do send cookies, so this // blocks cross-site authenticated sockets when the route opts in via `allowedOrigins`. const origin = req.headers.get("origin") if (handler.allowedOrigins !== undefined) { const allowed = typeof handler.allowedOrigins === "function" ? handler.allowedOrigins(origin) : origin !== null && handler.allowedOrigins.includes(origin) if (!allowed) return { kind: "reject", response: jsonError(403, "forbidden_origin") } } else if (origin !== null && !wsSameOrigin(origin, req)) { // Secure default (no explicit `allowedOrigins`): reject a CROSS-ORIGIN browser handshake - the // CSWSH case, since browsers send cookies on WS handshakes and don't apply CORS. Non-browser // clients send no `Origin` and pass; same-origin browsers pass. Set `allowedOrigins` to permit // specific cross-origin clients (or `() => true` for a genuinely public socket). return { kind: "reject", response: jsonError(403, "forbidden_origin") } } if (handler.upgrade === undefined) { return { kind: "upgrade", handler, data: undefined, pubsub, attach, maxPayloadBytes: this.wsMaxPayloadBytes, } } const upgradeSignal = getNeverAbortSignal() const ctx = new RequestContext( req, params, url.search, upgradeSignal, createUnboundedRequestBudget(upgradeSignal), platform, this.maxBodyBytes, this.protoPoisoning, ) const settle = (value: unknown): WebSocketUpgradeOutcome => value instanceof Response ? { kind: "reject", response: value } : { kind: "upgrade", handler, data: value, pubsub, attach, maxPayloadBytes: this.wsMaxPayloadBytes, } try { const result = handler.upgrade(ctx as unknown as WebSocketContext>) return result instanceof Promise ? result.then(settle, () => ({ kind: "reject" as const, response: jsonError(500, "internal_error"), })) : settle(result) } catch { return { kind: "reject", response: jsonError(500, "internal_error") } } } /** 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( req: Request, server: BunUpgradeServer, ): MaybePromise { const handle = (o: WebSocketUpgradeOutcome): MaybePromise => { if (o.kind === "pass") return this.fetch(req, bunPeerPlatform(server, req) as Platform>) if (o.kind === "reject") return o.response return server.upgrade(req, { data: { handler: o.handler, data: o.data } }) ? undefined : jsonError(426, "upgrade_required") } const outcome = this.resolveWebSocketUpgrade(req) return outcome instanceof Promise ? outcome.then(handle) : handle(outcome) } /** * 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( source: RequestSource, platform: Platform | undefined, ): Platform | undefined { const derived = resolveClientIp(platform?.clientIp, requestOf(source), this.clientIpTrust) return { ...platform, clientIp: derived } } private dispatch( source: RequestSource, platform: Platform | undefined, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, onTimeout: () => T, // True only from the Web `fetch` path - unlocks each route's fused lane, whose output type IS // `Response` (`T = Response` there by construction; the node path always passes false). webFast: boolean, ): MaybePromise { // Resolve the trust declaration into the platform's `clientIp` ONCE, here at the shared funnel, so // `c.clientIp` (and every hook/derive downstream) sees the derived caller. No config ⇒ the raw // socket peer the adapter supplied passes through untouched (a one-property no-op on the hot path). const resolved = this.clientIpTrust === undefined ? platform : this.deriveClientIp(source, platform) // onRequest hooks may be async, so a hooked app takes the async path; with no hooks (the common // case) routing stays synchronous, letting a bare route resolve with no lifecycle promise at all. if (this.onRequestHooks.length === 0) { return this.routeAndRun(source, resolved, finalize, wrapResponse, onTimeout, webFast) } if (!webFast && this.canUseNodeRequestHooks()) { return this.runWithNodeRequest(source, resolved, finalize, wrapResponse, onTimeout) } return this.runWithOnRequest(source, resolved, finalize, wrapResponse, onTimeout, webFast) } /** 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( source: RequestSource, platform: Platform | undefined, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, onTimeout: () => T, ): MaybePromise { const hooks = this.onNodeRequestHooks const request = this.nodeRequestContextOf(source) for (let i = 0; i < hooks.length; i++) { const hook = hooks[i] as NodeRequestHook const outcome = hook(request, platform) if (outcome instanceof Promise) { return outcome.then((early) => this.continueNodeRequest( early, i + 1, request, source, platform, finalize, wrapResponse, onTimeout, ), ) } if (outcome !== undefined) return wrapResponse(outcome) } return this.routeAndRun(source, platform, finalize, wrapResponse, onTimeout, false) } private async continueNodeRequest( first: Response | undefined, nextIndex: number, request: NodeRequestContext, source: RequestSource, platform: Platform | undefined, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, onTimeout: () => T, ): Promise { if (first !== undefined) return wrapResponse(first) for (let i = nextIndex; i < this.onNodeRequestHooks.length; i++) { const hook = this.onNodeRequestHooks[i] as NodeRequestHook const outcome = hook(request, platform) const early = outcome instanceof Promise ? await outcome : outcome if (early !== undefined) return wrapResponse(early) } return this.routeAndRun(source, platform, finalize, wrapResponse, onTimeout, false) } private canUseNodeRequestHooks(): boolean { return this.onRequestHooks.length > 0 && this.nodeRequestHooksComplete } /** * 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( source: RequestSource, platform: Platform | undefined, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, onTimeout: () => T, webFast: boolean, ): MaybePromise { const hooks = this.onRequestHooks const originalRequest = requestOf(source) // A Web Request is already the exact object visible to the hooks and the response walk. Only // adapter sources need the source→request mapping; avoiding the WeakMap write keeps the common // Bun/Deno/edge lifecycle allocation-light while preserving Node's lazy Request identity. if (source !== originalRequest) this.responseSources.set(source as object, originalRequest) let current: RequestSource = source for (let i = 0; i < hooks.length; i++) { const outcome = (hooks[i] as RawOnRequest)(requestOf(current), platform) if (outcome instanceof Promise) { return outcome.then((early) => this.continueOnRequest( early, i + 1, originalRequest, current, platform, finalize, wrapResponse, onTimeout, webFast, ), ) } if (outcome instanceof Request) { current = outcome if (outcome !== originalRequest) this.responseRequests.set(originalRequest, outcome) continue } if (outcome !== undefined) return wrapResponse(outcome) } return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast) } /** 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 async continueOnRequest( first: OnRequestResult, nextIndex: number, originalRequest: Request, sourceAtAwait: RequestSource, platform: Platform | undefined, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, onTimeout: () => T, webFast: boolean, ): Promise { let current = sourceAtAwait let early = first let index = nextIndex for (;;) { if (early instanceof Request) { current = early if (early !== originalRequest) this.responseRequests.set(originalRequest, early) } else if (early !== undefined) { return wrapResponse(early) } if (index >= this.onRequestHooks.length) break const outcome = (this.onRequestHooks[index] as RawOnRequest)(requestOf(current), platform) early = outcome instanceof Promise ? await outcome : outcome index++ } return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast) } private takeResponseRequest(source: RequestSource): Request { const tracked = this.responseSources.get(source as object) if (tracked !== undefined) { this.responseSources.delete(source as object) const rewritten = this.responseRequests.get(tracked) if (rewritten !== undefined) { this.responseRequests.delete(tracked) return rewritten } return tracked } const request = requestOf(source) const rewritten = this.responseRequests.get(request) if (rewritten === undefined) return request this.responseRequests.delete(request) return rewritten } /** * 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(source: RequestSource): NodeRequestContext { if (source.header !== undefined) return source as unknown as NodeRequestContext let context = this.nodeContexts.get(source as object) if (context === undefined) { const request = requestOf(source) context = { method: request.method, url: request.url, header: (name) => request.headers.get(name), } this.nodeContexts.set(source as object, context) } return context } /** Preserve the request visible to generic onRequest hooks for paired native response hooks. */ private takeNodeResponseRequest(source: RequestSource): NodeRequestContext { const tracked = this.responseSources.get(source as object) if (tracked !== undefined) { this.responseSources.delete(source as object) const rewritten = this.responseRequests.get(tracked) if (rewritten !== undefined) { this.responseRequests.delete(tracked) return { method: rewritten.method, url: rewritten.url, header: (name) => rewritten.headers.get(name), } } return { method: tracked.method, url: tracked.url, header: (name) => tracked.headers.get(name), } } return this.nodeRequestContextOf(source) } /** * 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( source: RequestSource, platform: Platform | undefined, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, onTimeout: () => T, webFast: boolean, ): MaybePromise { // Routing only needs the pathname. Scan the URL once but keep the two slices in locals instead // of allocating the `{ pathname, search }` pair returned by the public helper on every request. let pathname: string let search: string // Read the source's split ONCE. `urlParts` is an accessor on the Node request sources that // rescans the target and allocates a fresh pair every time it is touched, so testing it and then // reading each half through the accessor scanned the URL three times per request and threw two // of the three pairs away immediately. const parts = source.urlParts if (parts !== undefined) { pathname = parts.pathname search = parts.search } else { const rawUrl = source.url const schemeEnd = rawUrl.indexOf("://") const start = schemeEnd === -1 ? rawUrl.indexOf("/") : rawUrl.indexOf("/", schemeEnd + 3) if (start === -1) { pathname = "/" search = "" } else { let pathEnd = rawUrl.length let searchStart = -1 let searchEnd = rawUrl.length for (let i = start; i < rawUrl.length; i++) { const c = rawUrl.charCodeAt(i) if (c === 63 /* ? */ && searchStart === -1) { pathEnd = i searchStart = i } else if (c === 35 /* # */) { if (searchStart === -1) pathEnd = i searchEnd = i break } } pathname = rawUrl.slice(start, pathEnd) search = searchStart === -1 ? "" : rawUrl.slice(searchStart, searchEnd) } } const match = this.catalog.find(source.method, pathname) if (!match.found) { const mounted = this.fetchMount(pathname, source, platform) if (mounted !== undefined) { return mounted instanceof Promise ? mounted.then((response) => wrapResponse(response)) : wrapResponse(mounted) } if (match.reason === "method-not-allowed") { return wrapResponse( // Lowercase on purpose: a plain render's headers go into the node outcome record verbatim, // and every other name in that record is lowercase (the Web lane normalizes either way). plainError(405, "method_not_allowed", { allow: match.allowed.join(", ") }), ) } return wrapResponse(plainError(404, "not_found")) } // Inspect only captured values for escapes. Scanning the full pathname repeated work the router // already did and made every plain dynamic route pay for unrelated static path bytes. const params = match.params === EMPTY_PARAMS ? match.params : decodeRouteParams(match.params) if (params === null) { return wrapResponse(plainError(400, "malformed_path")) } return this.runMatched( source, platform, match.payload, params, search, finalize, wrapResponse, onTimeout, webFast, ) } private fetchMount( pathname: string, source: RequestSource, platform: Platform | undefined, ): MaybePromise | undefined { for (const mount of this.fetchMounts) { if (!underMountPrefix(pathname, mount.path)) continue const request = requestOf(source) return mount.handler( mount.stripPrefix ? stripMountPrefix(request, mount.path) : request, platform, ) } return undefined } /** Run a route that has already been matched by the runtime or Nifra's portable router. */ private runMatched( source: RequestSource, platform: Platform | undefined, entry: RouteEntry, params: Record, search: string | undefined, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, onTimeout: () => T, webFast: boolean, ): MaybePromise { // The route's transport byte cap: one property write here; the capped shadowing of direct // `c.req` body readers happens lazily at `c.req` access (`applyTransportCap`), so a route that // never direct-reads pays nothing. Framework readers bypass the shadow (`rawBodySourceOf`) and // keep their own caps, so `c.boundedBody(explicit)` still overrides upward. // `bodyLimit: "unlimited"` (undefined here) skips the cap entirely. // A schema route consumes and validates the body through `readBodyInput` before any derive or // handler can reach `c.req`; that lane already enforces `entry.bodyLimit`. Installing the full // direct-reader cap on `c.req` here would allocate bound readers, closures, and a stream wrapper // for a body that is already consumed. Keep the lazy transport cap for raw-body routes, where a // user read is the only framework-owned body boundary. if (entry.bodyLimit !== undefined && entry.schema?.body === undefined) { const method = source.method if (method !== "GET" && method !== "HEAD") markTransportCap(source, entry.bodyLimit) } // An idempotency route runs its dedupe lane first; on a fresh key it delegates to the normal lanes // (with the body buffered). All non-idempotent routes skip straight to the lanes - no added cost. // The runtime is always present when a route resolved idempotency (enforced at registration). if (entry.idempotent !== undefined && this.idempotencyRuntime !== undefined) { return this.idempotencyRuntime.run( entry.idempotent, requestOf(source), platform, entry, params, search, wrapResponse, { maxBodyBytes: entry.bodyLimit ?? UNLIMITED_BODY_BYTES, runLanes: (buffered, plat, ent, prm, srch) => this.idempotencyRunLanes(buffered, plat, ent as RouteEntry, prm, srch), }, ) } return this.runMatchedLanes( source, platform, entry, params, search, finalize, wrapResponse, onTimeout, webFast, ) } /** Supply request-specific deadline state to the route's precompiled execution plan. */ private runMatchedLanes( source: RequestSource, platform: Platform | undefined, entry: RouteEntry, params: Record, search: string | undefined, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, onTimeout: () => T, webFast: boolean, ): MaybePromise { // Translate the absolute wire deadline once, clamp it to local policy, then use the resulting // duration for both c.signal and c.budget. A client can only shorten work, never extend it. // Most requests have neither a local timeout nor a propagated deadline. Detect that case with // one header lookup and skip policy validation, wall-clock sampling, and admission objects. A // present wire deadline still goes through the full fail-closed parser/clamp below. const admission = !this.acceptInboundDeadlines ? this.requestTimeoutMs === 0 ? undefined : { ok: true as const, inherited: false, timeoutMs: this.requestTimeoutMs, deadline: Math.floor(Date.now() + this.requestTimeoutMs), } : this.requestTimeoutMs === 0 && headerOf(source, NIFRA_DEADLINE_HEADER) === null ? undefined : admitDeadline(source.headers, this.deadlineAdmissionOptions) if (admission !== undefined && !admission.ok) { return wrapResponse(plainError(admission.status, admission.reason)) } const effectiveTimeoutMs = admission?.timeoutMs ?? 0 // Only allocate a controller for a finite budget; the historical no-timeout path remains // allocation-light and exposes an unbounded budget that is never propagated on the wire. let controller: AbortController | undefined let signal = getNeverAbortSignal() if (effectiveTimeoutMs > 0) { controller = new AbortController() signal = controller.signal } const budget = controller === undefined ? getUnboundedRequestBudget() : createRequestBudget({ deadline: admission!.deadline as number, signal }) const plan = entry.execution const nativeContext = controller === undefined const outcome: MaybePromise = webFast && plan.fusedWeb !== undefined ? (plan.fusedWeb( source, params, search, signal, budget, platform, nativeContext, ) as MaybePromise) : !webFast && plan.fusedBody !== undefined ? plan.fusedBody( source, params, search, signal, budget, platform, false, finalize, wrapResponse, ) : plan.run( this, entry, source, params, search, signal, budget, platform, nativeContext, finalize, wrapResponse, ) // The request timeout only bounds work that is actually pending - a synchronous (bare) result is // already complete and can't time out, so it's returned as-is (no 503 race, no promise). if (controller !== undefined && outcome instanceof Promise) { const timedOut = admission?.inherited === true ? () => wrapResponse(plainError(504, "deadline_exceeded")) : onTimeout return this.withTimeout( outcome, controller, timedOut, Math.max(0, Math.ceil(budget.remaining())), ) } return outcome } /** @internal Symbol-keyed install seam for the `effectLedger()` plugin. Off the public typed surface. */ [INSTALL_EFFECT_LEDGER](runtime: EffectLedgerRuntime): void { this.assertConfigurable("effectLedger()") this.effectLedgerRuntime = runtime } /** 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. */ // @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts) private runContextlessBare( entry: RouteEntry, source: RequestSource, params: Record, search: string | undefined, signal: AbortSignal, budget: RequestBudget, platform: Platform | undefined, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, ): MaybePromise { let result: unknown try { result = (entry.handler as unknown as ContextlessHandler)() } catch (err) { return this.contextlessBareError( err, source, params, search, signal, budget, platform, wrapResponse, ) } if (result instanceof Promise) { return result.then( (value) => finalize(value, EMPTY_RESPONSE_CONTROLS), (err) => this.contextlessBareError( err, source, params, search, signal, budget, platform, wrapResponse, ), ) } return finalize(result, EMPTY_RESPONSE_CONTROLS) } private contextlessBareError( err: unknown, source: RequestSource, params: Record, search: string | undefined, signal: AbortSignal, budget: RequestBudget, platform: Platform | undefined, wrapResponse: (response: Response | ResponseResult) => T, ): T { if (err instanceof Response) return wrapResponse(err) // Handed over as the plain data it is: `wrapResponse` renders it on the JSON lane (no `Response` // built on Node). There is no context here by construction, so there is no `c.set` to merge. if (isResponseResult(err)) return wrapResponse(err) const ctx = new RequestContext( source, params, search, signal, budget, platform, this.maxBodyBytes, this.protoPoisoning, ) this.logRequestError(err, ctx) return this.internalErrorResponse(wrapResponse) } /** * 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`. */ // @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts) private runBare( entry: RouteEntry, ctx: RawContext, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, ): MaybePromise { let result: unknown try { if (entry.hasDecorations) Object.assign(ctx, entry.decorations) result = entry.handler(ctx) } catch (err) { return this.bareError(err, ctx, finalize, wrapResponse) } if (result instanceof Promise) { // Async handler on an otherwise-bare route: finish on a microtask, with the same error handling. return result.then( (value) => finalize(value, responseSet(ctx)), (err) => this.bareError(err, ctx, finalize, wrapResponse), ) } return finalize(result, responseSet(ctx)) } /** 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( handler: InternalHandler, decorations: Record | undefined, contextless: boolean, maxBodyBytes: number, ): FusedWebRunner { // Same thrown-value contract as the generic lane (`bare-error-lane.ts` via `bareError`); the fused // lane only injects its renderer, so a thrown `status(...)` and the flat 500 fold in static headers // through `wrapWebResponse`. One source of truth for the error surface, no per-lane copy to drift. const logError = (err: unknown, ctx: RawContext): Response => this.bareError( err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse, ) if (contextless && decorations === undefined) { // `() => ...` can't observe the context - skip allocating one entirely (errors still build // one for the structured log, exactly like runContextlessBare). const contextlessHandler = handler as unknown as ContextlessHandler return (source, params, search, signal, budget, platform, nativeContext) => { let result: unknown try { result = contextlessHandler() } catch (err) { return logError( err, nativeContext ? RequestContext.native( source, params, search, maxBodyBytes, platform, this.protoPoisoning, ) : new RequestContext( source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning, ), ) } if (result instanceof Promise) { return result.then( (value) => fusedRespondNoSet( value, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0, ), (err) => logError( err, nativeContext ? RequestContext.native( source, params, search, maxBodyBytes, platform, this.protoPoisoning, ) : new RequestContext( source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning, ), ), ) } return fusedRespondNoSet( result, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0, ) } } return (source, params, search, signal, budget, platform, nativeContext) => { const ctx = nativeContext ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning) : new RequestContext( source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning, ) if (decorations !== undefined) Object.assign(ctx, decorations) let result: unknown try { result = handler(ctx) } catch (err) { return logError(err, ctx) } if (result instanceof Promise) { return result.then( (value) => fusedRespond( value, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0, ), (err) => logError(err, ctx), ) } return fusedRespond( result, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0, ) } } /** 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( handler: InternalHandler, bodySchema: StandardSchemaV1, decorations: Record | undefined, maxBodyBytes: number, ): FusedBodyRunner { const logError = ( err: unknown, ctx: RawContext, finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T, wrapResponse: (response: Response | ResponseResult) => T, ): T => this.bareError(err, ctx, finalize, wrapResponse) const runHandler = ( ctx: RawContext, finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T, wrapResponse: (response: Response | ResponseResult) => T, ): MaybePromise => { if (decorations !== undefined) Object.assign(ctx, decorations) let output: MaybePromise try { output = handler(ctx) } catch (err) { return logError(err, ctx, finalize, wrapResponse) } if (output instanceof Promise) { return output.then( (result) => { try { return finalize(result, responseSet(ctx), ctx) } catch (err) { return logError(err, ctx, finalize, wrapResponse) } }, (err) => logError(err, ctx, finalize, wrapResponse), ) } try { return finalize(output, responseSet(ctx), ctx) } catch (err) { return logError(err, ctx, finalize, wrapResponse) } } const runValidated = ( result: StandardResult, ctx: RawContext, finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T, wrapResponse: (response: Response | ResponseResult) => T, ): MaybePromise => { if (result.issues !== undefined) return wrapResponse(plainValidationError(result.issues)) ctx.body = result.value return runHandler(ctx, finalize, wrapResponse) } const runParsed = ( parsed: unknown, ctx: RawContext, finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T, wrapResponse: (response: Response | ResponseResult) => T, ): MaybePromise => { let validation: StandardResult | Promise> try { validation = bodySchema["~standard"].validate(parsed) } catch (err) { return logError(err, ctx, finalize, wrapResponse) } if (validation instanceof Promise) { return validation.then( (settled) => { try { return runValidated(settled, ctx, finalize, wrapResponse) } catch (err) { return logError(err, ctx, finalize, wrapResponse) } }, (err) => logError(err, ctx, finalize, wrapResponse), ) } try { return runValidated(validation, ctx, finalize, wrapResponse) } catch (err) { return logError(err, ctx, finalize, wrapResponse) } } return ( source: RequestSource, params: Record, search: string | undefined, signal: AbortSignal, budget: RequestBudget, platform: Platform | undefined, nativeContext: boolean, finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T, wrapResponse: (response: Response | ResponseResult) => T, ): MaybePromise => { const ctx = nativeContext ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning) : new RequestContext( source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning, ) const finish = (value: unknown): MaybePromise => runParsed(value, ctx, finalize, wrapResponse) return readBodyFramed( source, maxBodyBytes, this.protoPoisoning, finish, wrapResponse, (err) => logError(err, ctx, finalize, wrapResponse), ) } } /** 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(body: FusedBodyRunner): FusedWebRunner { return (source, params, search, signal, budget, platform, nativeContext) => body( source, params, search, signal, budget, platform, nativeContext, (result, _set, ctx) => fusedRespond( result, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0, ), this.wrapWebResponse, ) as MaybePromise } /** 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( handler: InternalHandler, decorations: Record | undefined, querySchema: StandardSchemaV1, maxBodyBytes: number, ): FusedWebRunner { // Same thrown-value contract as the generic lane (`bare-error-lane.ts` via `bareError`); the fused // lane only injects its renderer, so a thrown `status(...)` and the flat 500 fold in static headers // through `wrapWebResponse`. One source of truth for the error surface, no per-lane copy to drift. const logError = (err: unknown, ctx: RawContext): Response => this.bareError( err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse, ) const runHandler = (ctx: RawContext, value: unknown): MaybePromise => { ctx.query = value let result: unknown try { result = handler(ctx) } catch (err) { return logError(err, ctx) } if (result instanceof Promise) { return result.then( (settled) => fusedRespond( settled, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0, ), (err) => logError(err, ctx), ) } return fusedRespond( result, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0, ) } return (source, params, search, signal, budget, platform, nativeContext) => { const ctx = nativeContext ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning) : new RequestContext( source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning, ) if (decorations !== undefined) Object.assign(ctx, decorations) let validation: MaybePromise> try { validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH])) } catch (err) { return logError(err, ctx) } if (validation instanceof Promise) { return validation.then( (settled) => settled.issues !== undefined ? this.wrapWebResponse(plainValidationError(settled.issues)) : runHandler(ctx, settled.value), (err) => logError(err, ctx), ) } if (validation.issues !== undefined) return this.wrapWebResponse(plainValidationError(validation.issues)) return runHandler(ctx, validation.value) } } /** * 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( handler: InternalHandler, derive: RawDerive, before: RawBeforeHandle, querySchema: StandardSchemaV1 | undefined, maxBodyBytes: number, ): FusedWebRunner { const logError = (err: unknown, ctx: RawContext): Response => this.bareError( err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse, ) return (source, params, search, signal, budget, platform, nativeContext) => { const ctx = nativeContext ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning) : new RequestContext( source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning, ) const runHandler = (): MaybePromise => { let result: unknown try { result = handler(ctx) } catch (err) { return logError(err, ctx) } if (result instanceof Promise) { return result.then( (value) => fusedRespond( value, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0, ), (err) => logError(err, ctx), ) } return fusedRespond( result, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0, ) } const runBefore = (): MaybePromise => { let early: unknown try { early = before(ctx) } catch (err) { return logError(err, ctx) } if (early instanceof Promise) { return early.then( (settled) => { if (settled === undefined) return runHandler() return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders) }, (err) => logError(err, ctx), ) } if (early === undefined) return runHandler() return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders) } const runDerive = (): MaybePromise => { let derived: unknown try { derived = derive(ctx) } catch (err) { return logError(err, ctx) } if (derived instanceof Promise) { return derived.then( (settled) => { if (isResponseResult(settled) || settled instanceof Response) { return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders) } Object.assign(ctx, settled as object) return runBefore() }, (err) => logError(err, ctx), ) } if (isResponseResult(derived) || derived instanceof Response) { return fusedRespond(derived, ctx, this.responseBodyTag, this.staticResponseHeaders) } Object.assign(ctx, derived as object) return runBefore() } if (querySchema === undefined) return runDerive() let validation: MaybePromise> try { validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH])) } catch (err) { return logError(err, ctx) } if (validation instanceof Promise) { return validation.then( (settled) => { if (settled.issues !== undefined) return this.wrapWebResponse(plainValidationError(settled.issues)) ctx.query = settled.value return runDerive() }, (err) => logError(err, ctx), ) } if (validation.issues !== undefined) return this.wrapWebResponse(plainValidationError(validation.issues)) ctx.query = validation.value return runDerive() } } /** * 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( handler: InternalHandler, derive: RawDerive, before: RawBeforeHandle, after: RawAfterHandle, querySchema: StandardSchemaV1 | undefined, maxBodyBytes: number, ): FusedWebRunner { const logError = (err: unknown, ctx: RawContext): Response => this.bareError( err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse, ) return (source, params, search, signal, budget, platform, nativeContext) => { const ctx = nativeContext ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning) : new RequestContext( source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning, ) const runAfter = (result: unknown): MaybePromise => { let transformed: unknown try { transformed = after(result, ctx) } catch (err) { return logError(err, ctx) } if (transformed instanceof Promise) { return transformed.then( (value) => fusedRespond( value, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0, ), (err) => logError(err, ctx), ) } return fusedRespond( transformed, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0, ) } const runHandler = (): MaybePromise => { let result: unknown try { result = handler(ctx) } catch (err) { return logError(err, ctx) } if (result instanceof Promise) { return result.then( (value) => runAfter(value), (err) => logError(err, ctx), ) } return runAfter(result) } const runBefore = (): MaybePromise => { let early: unknown try { early = before(ctx) } catch (err) { return logError(err, ctx) } if (early instanceof Promise) { return early.then( (settled) => { if (settled === undefined) return runHandler() return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders) }, (err) => logError(err, ctx), ) } if (early === undefined) return runHandler() return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders) } const runDerive = (): MaybePromise => { let derived: unknown try { derived = derive(ctx) } catch (err) { return logError(err, ctx) } if (derived instanceof Promise) { return derived.then( (settled) => { if (isResponseResult(settled) || settled instanceof Response) { return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders) } Object.assign(ctx, settled as object) return runBefore() }, (err) => logError(err, ctx), ) } if (isResponseResult(derived) || derived instanceof Response) { return fusedRespond(derived, ctx, this.responseBodyTag, this.staticResponseHeaders) } Object.assign(ctx, derived as object) return runBefore() } if (querySchema === undefined) return runDerive() let validation: MaybePromise> try { validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH])) } catch (err) { return logError(err, ctx) } if (validation instanceof Promise) { return validation.then( (settled) => { if (settled.issues !== undefined) return this.wrapWebResponse(plainValidationError(settled.issues)) ctx.query = settled.value return runDerive() }, (err) => logError(err, ctx), ) } if (validation.issues !== undefined) return this.wrapWebResponse(plainValidationError(validation.issues)) ctx.query = validation.value return runDerive() } } /** * 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( handler: InternalHandler, derive: RawDerive, before: RawBeforeHandle, after: RawAfterHandle | undefined, bodySchema: StandardSchemaV1, maxBodyBytes: number, ): FusedWebRunner { const logError = (err: unknown, ctx: RawContext): Response => this.bareError( err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse, ) return (source, params, search, signal, budget, platform, nativeContext) => { const ctx = nativeContext ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning) : new RequestContext( source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning, ) const runHandlerValidated = (): MaybePromise => { const runAfter = (result: unknown): MaybePromise => { if (after === undefined) { return fusedRespond( result, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0, ) } let transformed: unknown try { transformed = after(result, ctx) } catch (err) { return logError(err, ctx) } if (transformed instanceof Promise) { return transformed.then( (value) => fusedRespond( value, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0, ), (err) => logError(err, ctx), ) } return fusedRespond( transformed, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0, ) } const runHandler = (): MaybePromise => { let result: unknown try { result = handler(ctx) } catch (err) { return logError(err, ctx) } if (result instanceof Promise) { return result.then( (value) => runAfter(value), (err) => logError(err, ctx), ) } return runAfter(result) } const runBefore = (): MaybePromise => { let early: unknown try { early = before(ctx) } catch (err) { return logError(err, ctx) } if (early instanceof Promise) { return early.then( (settled) => { if (settled === undefined) return runHandler() return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders) }, (err) => logError(err, ctx), ) } if (early === undefined) return runHandler() return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders) } let derived: unknown try { derived = derive(ctx) } catch (err) { return logError(err, ctx) } if (derived instanceof Promise) { return derived.then( (settled) => { if (isResponseResult(settled) || settled instanceof Response) { return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders) } Object.assign(ctx, settled as object) return runBefore() }, (err) => logError(err, ctx), ) } if (isResponseResult(derived) || derived instanceof Response) { return fusedRespond(derived, ctx, this.responseBodyTag, this.staticResponseHeaders) } Object.assign(ctx, derived as object) return runBefore() } const onParsed = (parsed: unknown): MaybePromise => { let validation: StandardResult | Promise> try { validation = bodySchema["~standard"].validate(parsed) } catch (err) { return logError(err, ctx) } if (validation instanceof Promise) { return validation.then( (settled) => { if (settled.issues !== undefined) return this.wrapWebResponse(plainValidationError(settled.issues)) ctx.body = settled.value return runHandlerValidated() }, (err) => logError(err, ctx), ) } if (validation.issues !== undefined) return this.wrapWebResponse(plainValidationError(validation.issues)) ctx.body = validation.value return runHandlerValidated() } return readBodyFramed( source, maxBodyBytes, this.protoPoisoning, onParsed, (response) => this.wrapWebResponse(response), (err) => Promise.resolve(logError(err, ctx)), ) as MaybePromise } } private bareError( err: unknown, ctx: RawContext, finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T, wrapResponse: (response: Response | ResponseResult) => T, ): T { // The thrown-value contract lives in `bare-error-lane.ts`, one source of truth for both this // lane and the fused body runner. `responseSet` and the logger are injected because they reach // into class state; the closure is allocated only on the throw path, never on the hot lane. return renderBareError( err, ctx, finalize, wrapResponse, responseSet, (e, c) => this.logRequestError(e, c), () => this.internalErrorResponse(wrapResponse), ) } /** * 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(wrapResponse: (response: Response | ResponseResult) => T): T { return wrapResponse(plainError(500, "internal_error")) } // @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts) private runWithAround( entry: RouteEntry, ctx: RawContext, run: () => MaybePromise, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, ): MaybePromise { let outcome: MaybePromise try { outcome = this.runAround(entry.around, ctx, run) } catch (err) { return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse) } return outcome instanceof Promise ? outcome.catch((err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)) : outcome } /** Execute the registration-compiled general stage program. */ // @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts) private runProgram( entry: RouteEntry, source: RequestSource, ctx: RawContext, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, ): MaybePromise { return executeRouteProgram(this, entry, entry.program, source, ctx, finalize, wrapResponse) } /** Bind a compiled validation stage to its stable schema and the existing recovery contract. */ // @ts-expect-error TS6133 -- invoked structurally by the general route program private validateProgramStage( entry: RouteEntry, stage: Extract, source: RequestSource, ctx: RawContext, ): MaybePromise { const input = stage.kind === "headers" ? headerObjectOf(source.headers) : stage.kind === "params" ? ctx.params : stage.kind === "query" ? queryObjectOf(ctx[CONTEXT_SEARCH]) : undefined if (stage.kind === "body") return this.readProgramBody(entry, source, ctx) const validation = stage.schema["~standard"].validate(input) return validation instanceof Promise ? validation.then((result) => this.applyLifecycleValidation(entry, result, ctx, stage.kind)) : this.applyLifecycleValidation(entry, validation, ctx, stage.kind) } /** Preserve the existing bounded body reader and its fail-closed framing/prototype checks. */ private readProgramBody( entry: RouteEntry, source: RequestSource, ctx: RawContext, ): Promise { const bodySchema = entry.program.bodySchema if (bodySchema === undefined) return Promise.resolve(plainError(415, "unsupported_media_type")) return this.readAndValidateBody(source, entry, ctx, bodySchema, entry.program.bodyLimit) } /** Keep thrown Response/status and redacted 500 semantics in one existing error boundary. */ // @ts-expect-error TS6133 -- invoked structurally by the general route program private handleProgramError( entry: RouteEntry, error: unknown, ctx: RawContext, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, ): MaybePromise { return this.handleLifecycleError(entry, error, ctx, finalize, wrapResponse) } /** Finish a successful program result through response-contract enforcement and finalization. */ // @ts-expect-error TS6133 -- invoked structurally by the general route program private finishProgramResult( entry: RouteEntry, ctx: RawContext, result: unknown, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, ): MaybePromise { return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, result) } private runAround( hooks: ReadonlyArray, ctx: RawContext, run: () => MaybePromise, ): MaybePromise { const dispatch = (index: number): MaybePromise => { if (index >= hooks.length) return run() const hook = hooks[index]! let called = false return hook(ctx, () => { if (called) throw new Error("around next() called multiple times") called = true return dispatch(index + 1) }) } return dispatch(0) } // @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts) private runBodyOnly( entry: RouteEntry, source: RequestSource, ctx: RawContext, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, ): Promise { return readBodyFramed( source, entry.bodyLimit ?? UNLIMITED_BODY_BYTES, this.protoPoisoning, (parsed) => this.finishBodyOnly(entry, parsed, ctx, finalize, wrapResponse), wrapResponse, (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse), ) } /** 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( entry: RouteEntry, parsed: unknown, ctx: RawContext, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, ): MaybePromise { try { const bodySchema = entry.schema!.body! const validation = bodySchema["~standard"].validate(parsed) if (validation instanceof Promise) { return validation.then( (result) => { try { const outcome = this.applyBodyValidation(entry, result, ctx, finalize, wrapResponse) return outcome instanceof Promise ? outcome.catch((err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse), ) : outcome } catch (err) { return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse) } }, (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse), ) } const outcome = this.applyBodyValidation(entry, validation, ctx, finalize, wrapResponse) return outcome instanceof Promise ? outcome.catch((err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)) : outcome } catch (err) { return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse) } } private executeHandler( entry: RouteEntry, ctx: RawContext, finalize: (result: unknown, set: CtxSet) => T, wrapResponse?: (response: Response | ResponseResult) => T, ): MaybePromise { if (entry.hasDecorations) Object.assign(ctx, entry.decorations) const handlerOutput = entry.handler(ctx) if (handlerOutput instanceof Promise) { return handlerOutput.then( (value) => finalize(value, responseSet(ctx)), (err) => { if (wrapResponse) { return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse) } throw err }, ) } return finalize(handlerOutput, responseSet(ctx)) } private handleValidationErrorRecovery( entry: RouteEntry, recovery: unknown, ctx: RawContext, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, originalIssues: ReadonlyArray, kind: "body" | "query", ): MaybePromise { if (recovery !== undefined) { if (recovery instanceof Response) { return wrapResponse(recovery) } if (kind === "body" && entry.schema?.body) { const validation = entry.schema.body["~standard"].validate(recovery) if (validation instanceof Promise) { return validation.then((settled) => { if (settled.issues !== undefined) return wrapResponse(plainValidationError(settled.issues)) ctx.body = settled.value return this.executeHandler(entry, ctx, finalize) }) } if (validation.issues !== undefined) return wrapResponse(plainValidationError(validation.issues)) ctx.body = validation.value return this.executeHandler(entry, ctx, finalize) } if (kind === "query" && entry.schema?.query) { const validation = entry.schema.query["~standard"].validate(recovery) if (validation instanceof Promise) { return validation.then( (settled) => { if (settled.issues !== undefined) return wrapResponse(plainValidationError(settled.issues)) ctx.query = settled.value return this.executeHandler(entry, ctx, finalize, wrapResponse) }, (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse), ) } if (validation.issues !== undefined) return wrapResponse(plainValidationError(validation.issues)) ctx.query = validation.value return this.executeHandler(entry, ctx, finalize, wrapResponse) } } return wrapResponse(plainValidationError(originalIssues)) } private applyBodyValidation( entry: RouteEntry, result: StandardResult, ctx: RawContext, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, ): MaybePromise { if (result.issues !== undefined) { const hook = entry.schema?.onValidationError ?? this.defaultOnValidationError if (hook) { const recovery = hook(result.issues, ctx as unknown as Context, "body") if (recovery instanceof Promise) { return recovery.then((rec) => this.handleValidationErrorRecovery( entry, rec, ctx, finalize, wrapResponse, result.issues!, "body", ), ) } return this.handleValidationErrorRecovery( entry, recovery, ctx, finalize, wrapResponse, result.issues, "body", ) } return wrapResponse(plainValidationError(result.issues)) } ctx.body = result.value return this.executeHandler(entry, ctx, finalize) } // @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts) private runQueryOnly( entry: RouteEntry, ctx: RawContext, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, ): MaybePromise { try { // Call the validator directly for the raw StandardResult (read `.issues`/`.value`) - skip // `validateStandard`'s per-request wrapper-object allocation, mirroring the bodyOnly path. const validation = entry.schema!.query!["~standard"].validate( queryObjectOf(ctx[CONTEXT_SEARCH]), ) if (validation instanceof Promise) { return validation.then( (settled) => { try { const outcome = this.applyQueryValidation(entry, settled, ctx, finalize, wrapResponse) return outcome instanceof Promise ? outcome.catch((err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse), ) : outcome } catch (err) { return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse) } }, (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse), ) } return this.applyQueryValidation(entry, validation, ctx, finalize, wrapResponse) } catch (err) { return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse) } } /** Validate-result → set `ctx.query` → run handler. A method (not a per-request closure), the * query analogue of {@link applyBodyValidation}. */ private applyQueryValidation( entry: RouteEntry, result: StandardResult, ctx: RawContext, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, ): MaybePromise { if (result.issues !== undefined) { const hook = entry.schema?.onValidationError ?? this.defaultOnValidationError if (hook) { const recovery = hook(result.issues, ctx as unknown as Context, "query") if (recovery instanceof Promise) { return recovery.then((rec) => this.handleValidationErrorRecovery( entry, rec, ctx, finalize, wrapResponse, result.issues!, "query", ), ) } return this.handleValidationErrorRecovery( entry, recovery, ctx, finalize, wrapResponse, result.issues, "query", ) } return wrapResponse(plainValidationError(result.issues)) } ctx.query = result.value return this.executeHandler(entry, ctx, finalize, wrapResponse) } /** * 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(response: Response, req: Request): MaybePromise { const hooks = this.onResponseHooks let current = response for (let i = 0; i < hooks.length; i++) { const next = (hooks[i] as RawOnResponse)(current, req) if (next instanceof Promise) { return next.then((settled) => this.continueOnResponse(settled, i + 1, req)) } current = next } return current } private applyOnResponseAndFinalize(response: Response, req: Request): MaybePromise { try { const transformed = this.applyOnResponse(response, req) return transformed instanceof Promise ? transformed.then( (settled) => this.completeResponseFinalization({ response: settled }, req), (error) => this.failResponseFinalization(response, error, req), ) : this.completeResponseFinalization({ response: transformed }, req) } catch (error) { return this.failResponseFinalization(response, error, req) } } private completeResponseFinalization( outcome: ResponseFinalization, req: Request, ): MaybePromise { const notified = this.notifyResponseFinalized(outcome, req) return notified instanceof Promise ? notified.then(() => outcome.response) : outcome.response } private failResponseFinalization( response: Response, error: unknown, req: Request, ): Promise { const notified = this.notifyResponseFinalized({ response, error }, req) if (notified instanceof Promise) { return notified.then(() => { throw error }) } // A hook failure must surface as a REJECTION even when every prior step ran synchronously: // `fetch()` may now resolve without a promise, but its failure contract stays promise-shaped - // a synchronous throw here would escape `Promise.resolve(app.fetch(...))` bridges and // `.then()`-style callers entirely instead of reaching their rejection handling. return Promise.reject(error) } /** Notify terminal observers in order while isolating both sync and async failures. */ private notifyResponseFinalized(outcome: ResponseFinalization, req: Request): MaybePromise { let pending: Promise | undefined for (const hook of this.onResponseFinalizedHooks) { if (pending !== undefined) { pending = pending.then(async () => { try { await hook(outcome, req) } catch { // Terminal observation must never change request behavior. } }) continue } try { const result = hook(outcome, req) if (result instanceof Promise) pending = result.catch(() => {}) } catch { // Terminal observation must never change request behavior. } } return pending } /** Async tail of {@link applyOnResponse}: runs the remaining hooks once one has gone async. */ private async continueOnResponse( response: Response, nextIndex: number, req: Request, ): Promise { let current = response for (let i = nextIndex; i < this.onResponseHooks.length; i++) { const next = (this.onResponseHooks[i] as RawOnResponse)(current, req) current = next instanceof Promise ? await next : next } return current } /** * 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 async withTimeout( work: Promise, controller: AbortController, onTimeout: () => T, timeoutMs: number, ): Promise { let timer: ReturnType | undefined const timeout = new Promise((resolve) => { timer = setTimeout(() => { controller.abort() resolve(onTimeout()) }, timeoutMs) }) try { return await Promise.race([work, timeout]) } finally { if (timer !== undefined) clearTimeout(timer) } } private finishLifecycleContract( entry: RouteEntry, ctx: RawContext, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, result: unknown, ): MaybePromise { try { const contract = entry.responseContract if (contract === undefined) return finalize(result, responseSet(ctx)) const checked = contract.runtime.check(contract.schema, result) if (checked instanceof Promise) { return checked.then( (outcome) => this.finishContractOutcome(ctx, finalize, wrapResponse, outcome), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse), ) } return this.finishContractOutcome(ctx, finalize, wrapResponse, checked) } catch (err) { return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse) } } private finishContractOutcome( ctx: RawContext, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, outcome: { readonly kind: "ok" | "warn" | "violation" readonly value?: unknown readonly message?: string }, ): T { if (outcome.kind === "violation") { this.logger.error("response contract violation", { method: ctx.req.method, path: pathnameOf(ctx.req.url), detail: outcome.message, }) return wrapResponse(plainError(500, "internal_error")) } if (outcome.kind === "warn") { this.logger.warn("response contract", { method: ctx.req.method, path: pathnameOf(ctx.req.url), detail: outcome.message, }) } return finalize(outcome.value, responseSet(ctx)) } /** 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( entry: RouteEntry, err: unknown, ctx: RawContext, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, ): MaybePromise { // A *thrown* Response is deliberate control flow, not an error - a guard throws a redirect/401, // an action throws an error page. Return it as-is (Remix/SvelteKit semantics); don't run onError // or log it as a 500. This is what makes `throw redirect(...)` / `requireSession(...)` work from // any handler or loader. if (err instanceof Response) return wrapResponse(err) // Same rule for a thrown `status(...)`, but through `finalize` rather than `wrapResponse`: the // value is still plain data, so the ordinary JSON lane renders it and no `Response` is built. if (isResponseResult(err)) return finalize(err, responseSet(ctx)) if (entry.onError.length === 0) { // Never crash the server or leak internals. The client gets a flat 500; the detail goes to the // (redacting) logger. Body-read failures and around-hook failures land here too. this.logRequestError(err, ctx) return wrapResponse(plainError(500, "internal_error")) } return this.runErrorHooks(entry, err, ctx, finalize, wrapResponse) } /** 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 async runErrorHooks( entry: RouteEntry, err: unknown, ctx: RawContext, finalize: (result: unknown, set: CtxSet) => T, wrapResponse: (response: Response | ResponseResult) => T, ): Promise { // Indexed for the same reason as the `beforeHandle` chain above: the iterator would outlive // the `await` and stay heap-allocated on both engines. for (let i = 0; i < entry.onError.length; i++) { const outcome = entry.onError[i]!(err, ctx) const handled = outcome instanceof Promise ? await outcome : outcome if (handled !== undefined) return finalize(handled, responseSet(ctx)) } this.logRequestError(err, ctx) return wrapResponse(plainError(500, "internal_error")) } /** 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(err: unknown, ctx: RawContext): void { emitRequestErrorLog(this.logger, this.errorLogDetail, err, ctx) } private readAndValidateBody( req: RequestSource, entry: RouteEntry, ctx: RawContext, bodySchema: StandardSchemaV1 = entry.schema?.body as StandardSchemaV1, bodyLimit: number | undefined = entry.bodyLimit, ): Promise { // Keep the generic lifecycle body stage on the same framed parser/continuation as the body-only // lane. This avoids an async wrapper that first awaits parsing and then starts validation in a // second continuation, while retaining the exact bounded-read, content-type, and poisoning // contracts shared by every body route. const validate = (parsed: unknown): MaybePromise => { const validation = bodySchema["~standard"].validate(parsed) return validation instanceof Promise ? validation.then((result) => this.applyLifecycleValidation(entry, result, ctx, "body")) : this.applyLifecycleValidation(entry, validation, ctx, "body") } return readBodyFramed( req, bodyLimit ?? UNLIMITED_BODY_BYTES, this.protoPoisoning, validate, (response) => response, (error) => Promise.reject(error), ) } /** 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( entry: RouteEntry, result: StandardResult, ctx: RawContext, kind: "body" | "query" | "params" | "headers", ): MaybePromise { const assign = (value: unknown): void => { if (kind === "body") ctx.body = value else if (kind === "query") ctx.query = value else if (kind === "headers") ctx.headers = value as Record else ctx.params = value as Record } if (result.issues === undefined) { assign(result.value) return undefined } const hook = entry.schema?.onValidationError ?? this.defaultOnValidationError if (hook === undefined) return plainValidationError(result.issues) const attempted = hook(result.issues, ctx as unknown as Context, kind) if (attempted instanceof Promise) { return attempted.then((recovery) => this.finishLifecycleValidationRecovery(entry, kind, result.issues!, recovery, assign), ) } return this.finishLifecycleValidationRecovery(entry, kind, result.issues, attempted, assign) } private finishLifecycleValidationRecovery( entry: RouteEntry, kind: "body" | "query" | "params" | "headers", issues: ReadonlyArray, recovery: unknown, assign: (value: unknown) => void, ): MaybePromise { if (recovery === undefined) return plainValidationError(issues) if (recovery instanceof Response) return recovery const schema = kind === "body" ? entry.schema?.body : kind === "query" ? entry.schema?.query : kind === "params" ? entry.schema?.params : entry.schema?.headers const retried = schema!["~standard"].validate(recovery) if (retried instanceof Promise) { return retried.then((settled) => { if (settled.issues !== undefined) return plainValidationError(settled.issues) assign(settled.value) return undefined }) } if (retried.issues !== undefined) return plainValidationError(retried.issues) assign(retried.value) return undefined } /** 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( entry: RouteEntry, paramNames: readonly string[], fused: FusedWebRunner | undefined, signal: AbortSignal | undefined, budget: RequestBudget | undefined, ): BunNativeHandler { // This callback is reached only from Bun's compiled native route table. Bun has already parsed // the HTTP framing, so the JSON lane may retain its native fused `json()` parse without weakening // the portable/adapted source contract (those sources are never marked). const markFramed = (request: Request): void => markTrustedBodyFraming(request) // The fused native lane bypasses `runMatched`, so the route's transport byte cap must be // marked here too - otherwise a Bun `listen()` fused route would leave direct `c.req` body // reads uncapped. The non-fused branches go through `fetchMatched` -> `runMatched`, which marks. const bodyLimit = entry.schema?.body === undefined ? entry.bodyLimit : undefined const inner = fused const capped: FusedWebRunner | undefined = inner === undefined || bodyLimit === undefined ? inner : (source, params, search, fusedSignal, fusedBudget, platform, nativeContext) => { const method = source.method if (method !== "GET" && method !== "HEAD") markTransportCap(source, bodyLimit) return inner(source, params, search, fusedSignal, fusedBudget, platform, nativeContext) } if (paramNames.length === 0) { if (capped === undefined) { return (request) => { markFramed(request) return this.fetchMatched(request, entry, EMPTY_PARAMS) } } if (this.acceptInboundDeadlines) { return (request) => { markFramed(request) return request.headers.get(NIFRA_DEADLINE_HEADER) !== null ? this.fetchMatched(request, entry, EMPTY_PARAMS) : capped(request, EMPTY_PARAMS, undefined, signal!, budget!, undefined, true) } } return (request) => { markFramed(request) return capped(request, EMPTY_PARAMS, undefined, signal!, budget!, undefined, true) } } const malformed = paramNames.length === 1 ? (params: Record) => params[paramNames[0]!]?.includes("\uFFFD") === true : hasReplacementParam if (capped === undefined) { return (request) => { markFramed(request) const params = (request as BunRequestWithParams).params ?? EMPTY_PARAMS if (malformed(params)) return this.fetchSource(request) return this.fetchMatched(request, entry, params) } } if (this.acceptInboundDeadlines) { return (request) => { markFramed(request) const params = (request as BunRequestWithParams).params ?? EMPTY_PARAMS if (malformed(params)) return this.fetchSource(request) return request.headers.get(NIFRA_DEADLINE_HEADER) !== null ? this.fetchMatched(request, entry, params) : capped(request, params, undefined, signal!, budget!, undefined, true) } } return (request) => { markFramed(request) const params = (request as BunRequestWithParams).params ?? EMPTY_PARAMS if (malformed(params)) return this.fetchSource(request) return capped(request, params, undefined, signal!, budget!, undefined, true) } } /** 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(): BunNativeRoutes | undefined { // A `clientIp` trust declaration must run the resolver in `dispatch`, which the fused native lane // bypasses - so an app that declares trust routes through the fetch lane (where `c.clientIp` // resolves) instead of Bun's native table. The allocation-free default keeps native fusion. if ( this.onRequestHooks.length > 0 || this.wsRouteCount > 0 || this.clientIpTrust !== undefined ) { return undefined } const routes: BunNativeRoutes = Object.create(null) as BunNativeRoutes const mayUseFusedNative = this.requestTimeoutMs === 0 && this.onResponseHooks.length === 0 && this.onResponseFinalizedHooks.length === 0 && // The capacity gate must wrap every request; the fused lane bypasses fetchMatched, so enabling // admission drops fusion (native matching stays) and routes through the gated matched lane. this.capacityGate === undefined const unboundedSignal = mayUseFusedNative ? getNeverAbortSignal() : undefined const unboundedBudget = mayUseFusedNative ? getUnboundedRequestBudget() : undefined let count = 0 for (const { method, path, pattern, entry } of this.catalog.entries()) { if (pattern.segments.some((segment) => segment.kind === "wildcard")) continue let methods = routes[path] if (methods === undefined) { methods = Object.create(null) as BunNativeMethodTable routes[path] = methods } const paramNames = pattern.paramNames const fused = mayUseFusedNative ? entry.execution.fusedWeb : undefined methods[method] = this.compileBunNativeHandler( entry, paramNames, fused, unboundedSignal, unboundedBudget, ) count += 1 } if (count === 0) return undefined // RFC 9110 §9.3.2: a GET route answers HEAD with identical status + headers (Bun strips the // body on the wire). Alias the compiled GET handler under HEAD so HEAD stays on the native // lane instead of falling through to the portable dispatcher; the router applies the same // fallback there, so both lanes agree. An explicit HEAD registration wins via the ??=. for (const path of Object.keys(routes)) { const methods = routes[path]! if (methods.GET !== undefined) methods.HEAD ??= methods.GET } return routes } /** * 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 { if (typeof Bun === "undefined") { // listen() is the one Bun-specific seam. Off Bun, fail loud + actionable rather // than letting the Bun.serve call below throw a bare `ReferenceError: Bun is not // defined`. Exercised by the @nifrajs/deno suite (which runs on a non-Bun runtime). throw new FrameworkError( "BUN_REQUIRED", "listen() uses Bun.serve and runs only on Bun. Serve on Node with @nifrajs/node or on Deno with @nifrajs/deno, or hand app.fetch to any fetch-compatible runtime (Workers, etc.).", ) } // Seal before Bun.serve: in `"error"` mode this throws instead of leaving a bound port behind. if (hookAuditRuntime && process.env.NODE_ENV !== "production") sealHookAudit(this, this.catalog.size, this.logger) // Bun's `Server` is the concrete handle; we expose the stable `RunningServer` // subset so the public types don't depend on the ambient `Bun` global. The cast // bridges them - Bun's `.port` is `number | undefined` (undefined only for unix // sockets, never a TCP `listen`) and its `.stop` returns a promise we don't await. // Pass only the request - Bun's `fetch` 2nd arg is the Bun `Server`, not our `platform`. // With WS routes, hand Bun a `websocket` config + a fetch that upgrades matching requests (the // `server` 2nd arg is how Bun exposes `upgrade`); otherwise the lean request-only fetch. The // `websocket` handlers are one shared dispatcher - each connection's `ws.data.handler` is the // matched route's handler, set by `server.upgrade`. // With WS routes, the dispatcher comes from the installed `.use(websocket())` runtime - non-null // because wsRouteCount > 0 means ws() ran, and ws() requires the runtime at registration. // Native pub/sub when the app has WS routes and none validate outbound frames: `ws.subscribe` and // `app.publish` go through Bun's own (uWebSockets) broadcast instead of the JS registry loop. const nativePubsub = this.wsRouteCount > 0 && !this.wsHasValidatedSend const wsHandlers = this.wsRouteCount === 0 ? undefined : (this.wsRuntime as WsRuntime).bunHandlers(this.topics as TopicRegistry, nativePubsub) const reusePort = options?.reusePort === true // Spread rather than pass `hostname: undefined` - Bun treats an explicit undefined as a value // on some option paths, and omitting is what selects its 0.0.0.0 default. const bind = options?.hostname === undefined ? {} : { hostname: options.hostname } // Same reasoning as `bind`: omit rather than pass undefined, so Bun's own default applies. const idle = options?.idleTimeoutSec === undefined ? {} : { idleTimeout: options.idleTimeoutSec } const nativeRoutes = wsHandlers === undefined ? this.buildBunNativeRoutes() : undefined const running = (wsHandlers === undefined ? Bun.serve({ port, reusePort, ...bind, ...idle, ...(nativeRoutes === undefined ? {} : { routes: nativeRoutes }), fetch: (req: Request, server) => this.fetch(req, bunPeerPlatform(server, req) as Platform>), }) : Bun.serve({ port, reusePort, ...bind, ...idle, fetch: (req, server) => this.bunFetchWithWebSocket(req, server), // Bun's `ServerWebSocket` is runtime-compatible with the handlers' structural // `BunSocket` view (kept local so `Bun.*` types never leak into the published .d.ts); the // `unknown` params bridge a TS structural-variance quirk. Round-trip covered by websocket.test.ts. websocket: { // Cap inbound frames so a huge message can't be buffered/JSON-parsed into memory; the runtime // closes an over-cap connection before the handler runs. Default 1 MB (maxBodyBytes). maxPayloadLength: this.wsMaxPayloadBytes, open: (ws) => wsHandlers.open(ws), message: (ws, message) => wsHandlers.message(ws, message), close: (ws, code, reason) => wsHandlers.close(ws, code, reason), }, })) as unknown as RunningServer this.bunServer = running // Bind `app.publish` to Bun's native broadcast now that the server handle exists. Guarded on the // method's presence so a runtime whose handle lacks it simply keeps the registry path. if (nativePubsub && typeof running.publish === "function") { const native = running.publish.bind(running) this.nativePublish = (topic, data) => { native(topic, data) } } this.sealed = true if (this.gracefulSignals) this.installSignalHandlers() return running } /** * 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. */ async stop({ drainMs = DEFAULT_DRAIN_MS }: { drainMs?: number } = {}): Promise { const server = this.bunServer if (server === undefined) return this.bunServer = undefined const deadline = Date.now() + drainMs while (server.pendingRequests > 0 && Date.now() < deadline) { await Bun.sleep(DRAIN_POLL_MS) } server.stop(server.pendingRequests > 0) // force-close iff stragglers remain past the deadline if (this.stopHooks.length === 0) return const callbacks = this.stopHooks.map((hook) => Promise.resolve().then(hook)) const settled = Promise.allSettled(callbacks) await Promise.race([ settled, new Promise((resolve) => setTimeout(resolve, STOP_HOOK_TIMEOUT_MS)), ]) } private installSignalHandlers(): void { // Drain, then let the process exit naturally - the stopped server no longer // holds the event loop open. Opt-in (`gracefulSignals`), so taking over the // signals is consented; we don't force `process.exit`. const onSignal = (): void => { void this.stop() } process.once("SIGTERM", onSignal) process.once("SIGINT", onSignal) } } /** * 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 function server( options?: ServerOptions, ): Server { // `Env` is a phantom type-level marker: the runtime `env` arrives via `app.fetch(req, { env })` at // request time, not stored on the builder - so seed the context type with a cast (as `derive`/ // `decorate` do for their `Ctx` extensions). return new Server(options) as unknown as Server }