122 lines
4.0 KiB
TypeScript
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,
|
|
});
|
|
}
|