Files
WRNexusJS/packages/core/src/context.ts
T

122 lines
4.0 KiB
TypeScript

/**
* 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, string | number>) => 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<string, string>;
/**
* Per-request scratch space. Middleware can attach values here
* (e.g. the authenticated user) and downstream handlers can read them.
*/
locals: Record<string, unknown>;
/**
* 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> | 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> | Response;
/** SEO metadata rendered into the document `<head>`. */
export type SeoConfig = {
/** BCP 47 document language used on `<html lang>` (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 `<head>`. */
export type PageMeta = SeoConfig;
/** A page module's default export. Returns an HTML string for the body. */
export type PageComponent = (ctx: Context) => string | Promise<string>;
/** 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,
});
}