/** * Core request context and middleware contracts. * * The `Context` object is the single value that flows through middleware, * pages and API routes. It is intentionally small and framework-agnostic so * it can later be reused by the `.wrn` compiler output. */ import type { Tenant } from "./tenant.ts"; import type { Tracer } from "./observability.ts"; import { applyCookieHeaders, createCookieStore, createLocalStorageSnapshot, createSessionStore, type CookieStore, type LocalStorageSnapshot, type SessionStore, } from "./storage.ts"; /** Translate a key for the active language, interpolating `{param}` placeholders. */ export type TFunction = (key: string, params?: Record) => string; export interface PaginationOptions { defaultLimit?: number; maxLimit?: number; } export interface PaginationInput { limit: number; cursor?: string; } export interface Context< Params extends Record = Record, User = unknown, > { /** The raw incoming web-standard Request. */ req: Request; /** Parsed URL of the request (pathname, query, etc.). */ url: URL; /** Active language for this request (resolved by the runtime); "" if i18n is unused. */ lang: string; /** Translate a key for the active language (identity until the runtime sets it). */ t: TFunction; /** Dynamic route params, e.g. `/users/[id]` -> `{ id: "42" }`. */ params: Params; /** * Per-request scratch space. Middleware can attach values here * (e.g. the authenticated user) and downstream handlers can read them. */ locals: Record; /** Resource resolved by a declarative route guard, when present. */ resource?: unknown; /** * The authenticated user for this request, or null when anonymous. Populated * by the `sessionAuth` middleware (or `logIn`); read via `getUser(ctx)`. */ user?: User | null; /** Active tenant/workspace resolved by tenant middleware. */ tenant?: Tenant; /** Request tracer installed by observability middleware. */ tracer?: Tracer; /** * The direct socket peer IP, set by the server from `server.requestIP`. This * is NOT spoofable by request headers — prefer it over `x-forwarded-for` for * rate limiting unless you run behind a trusted proxy. */ ip?: string; /** Read/write HTTP cookies for the current response. */ cookies: CookieStore; /** In-memory cookie-backed session store. */ session: SessionStore; /** Read-only localStorage snapshot sent by the browser for CSR data bindings. */ localStorage: LocalStorageSnapshot; /** Parse and bound the standard `limit` and `cursor` query parameters. */ pagination(options?: PaginationOptions): PaginationInput; } /** Calls the next middleware in the chain (or the final route handler). */ export type Next = () => Promise | Response; /** * Middleware runs before pages and API routes. It can: * - inspect/modify `ctx` * - short-circuit by returning a `Response` without calling `next()` * - continue by returning `await next()` */ export type Middleware = (ctx: Context, next: Next) => Promise | Response; /** SEO metadata rendered into the document ``. */ export type SeoConfig = { /** BCP 47 document language used on `` (default: `en`). */ lang?: string; title?: string; titleTemplate?: string; description?: string; canonical?: string; canonicalBase?: string; robots?: string; keywords?: string | string[]; image?: string; siteName?: string; type?: string; locale?: string; twitterCard?: string; twitterSite?: string; themeColor?: string; }; /** Page metadata rendered into the document ``. */ export type PageMeta = SeoConfig; /** A page module's default export. Returns an HTML string for the body. */ export type PageComponent = (ctx: Context) => string | Promise; /** Create a fresh context for an incoming request. */ export function createContext = Record>( req: Request, url: URL, ): Context { // The caller supplies the validated public URL (including trusted proxies). // Request-based handlers must see the same origin as ctx.url. if (req.url !== url.href) req = new Request(url, req); const cookies = createCookieStore(req); return { req, url, params: {} as Params, locals: {}, lang: "", t: (key) => key, cookies, // `url` already reflects X-Forwarded-Proto when trustProxy is on, so session // cookies get `Secure` behind a TLS-terminating proxy (matches CSRF cookies). session: createSessionStore(cookies, req, undefined, url.protocol === "https:"), localStorage: createLocalStorageSnapshot(req), pagination(options = {}) { const defaultLimit = positivePaginationInteger(options.defaultLimit, 25); const maxLimit = positivePaginationInteger(options.maxLimit, 100); const requested = Number(url.searchParams.get("limit")); const limit = Number.isInteger(requested) && requested > 0 ? Math.min(requested, maxLimit) : Math.min(defaultLimit, maxLimit); const cursor = url.searchParams.get("cursor")?.trim() || undefined; return { limit, cursor }; }, }; } function positivePaginationInteger(value: number | undefined, fallback: number): number { return Number.isInteger(value) && Number(value) > 0 ? Number(value) : fallback; } /** Apply headers accumulated on the context, such as Set-Cookie. */ export function withContextHeaders(ctx: Context, res: Response): Response { const headers = new Headers(res.headers); applyCookieHeaders(ctx, headers); return new Response(res.body, { status: res.status, statusText: res.statusText, headers, }); }