/** * 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 { 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 type Context = { /** 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: Record; /** * Per-request scratch space. Middleware can attach values here * (e.g. the authenticated user) and downstream handlers can read them. */ locals: Record; /** * The authenticated user for this request, or null when anonymous. Populated * by the `sessionAuth` middleware (or `logIn`); read via `getUser(ctx)`. */ user?: unknown; /** * 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; }; /** 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(req: Request, url: URL): Context { const cookies = createCookieStore(req); return { req, url, 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), }; } /** 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, }); }