feat: centralize application framework primitives
Quality / quality (ubuntu-latest) (push) Failing after 14m38s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-22 23:07:46 +05:30
parent 96e082b943
commit a3ddd39b7b
73 changed files with 1429 additions and 84 deletions
+41 -6
View File
@@ -22,7 +22,20 @@ import {
/** Translate a key for the active language, interpolating `{param}` placeholders. */
export type TFunction = (key: string, params?: Record<string, string | number>) => string;
export type Context = {
export interface PaginationOptions {
defaultLimit?: number;
maxLimit?: number;
}
export interface PaginationInput {
limit: number;
cursor?: string;
}
export interface Context<
Params extends Record<string, string> = Record<string, string>,
User = unknown,
> {
/** The raw incoming web-standard Request. */
req: Request;
/** Parsed URL of the request (pathname, query, etc.). */
@@ -32,17 +45,19 @@ export type Context = {
/** 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>;
params: Params;
/**
* Per-request scratch space. Middleware can attach values here
* (e.g. the authenticated user) and downstream handlers can read them.
*/
locals: Record<string, unknown>;
/** 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?: unknown;
user?: User | null;
/** Active tenant/workspace resolved by tenant middleware. */
tenant?: Tenant;
/** Request tracer installed by observability middleware. */
@@ -59,7 +74,9 @@ export type Context = {
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> | Response;
@@ -99,12 +116,15 @@ export type PageMeta = SeoConfig;
export type PageComponent = (ctx: Context) => string | Promise<string>;
/** Create a fresh context for an incoming request. */
export function createContext(req: Request, url: URL): Context {
export function createContext<Params extends Record<string, string> = Record<string, string>>(
req: Request,
url: URL,
): Context<Params> {
const cookies = createCookieStore(req);
return {
req,
url,
params: {},
params: {} as Params,
locals: {},
lang: "",
t: (key) => key,
@@ -113,9 +133,24 @@ export function createContext(req: Request, url: URL): Context {
// 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);
+29
View File
@@ -33,6 +33,10 @@ export interface EndpointDefinition<I, O> {
input?: SchemaLike<I> | OutputSchemaLike<I>;
output?: SchemaLike<O> | OutputSchemaLike<O>;
auth?: "optional" | "required";
/** Permission checked through an installed @wrnexus/authz middleware. */
permission?: string;
/** Resource supplied to bound authorization policies. */
resource?: (ctx: Context, input: I) => unknown | Promise<unknown>;
description?: string;
tags?: string[];
handler(input: I, ctx: Context): O | Promise<O>;
@@ -111,6 +115,31 @@ export function defineEndpoint(
: await ctx.req.json().catch(() => ({}));
input = schemaValue(definition.input, resolvedInput);
}
if (definition.permission) {
const authz = (
ctx as Context & {
authz?: {
decide(
permission: string,
resource?: unknown,
): Promise<{ allowed: boolean; reason?: string }>;
};
}
).authz;
if (!authz) {
throw new EndpointError(
500,
"AUTHZ_NOT_CONFIGURED",
"Authorization middleware is not configured.",
);
}
const resource = definition.resource ? await definition.resource(ctx, input) : undefined;
const decision = await authz.decide(definition.permission, resource);
if (!decision.allowed) {
throw new EndpointError(403, "FORBIDDEN", "Permission denied.");
}
ctx.resource = resource;
}
const rawOutput = await definition.handler(input, ctx);
const output = definition.output ? schemaValue(definition.output, rawOutput) : rawOutput;
return output instanceof Response ? output : json({ data: output });
+3
View File
@@ -9,6 +9,8 @@ export type {
PageComponent,
SeoConfig,
TFunction,
PaginationOptions,
PaginationInput,
} from "./context.ts";
export { createContext, withContextHeaders } from "./context.ts";
export {
@@ -127,6 +129,7 @@ export {
export type {
CookieOptions,
CookieStore,
TransactionCookie,
LocalStorageSnapshot,
SessionStore,
SessionBackend,
+49 -1
View File
@@ -17,6 +17,16 @@ export interface CookieStore {
set(name: string, value: string, options?: CookieOptions): void;
delete(name: string, options?: CookieOptions): void;
headers(): string[];
transaction<T extends Record<string, unknown> = Record<string, unknown>>(
name: string,
options?: CookieOptions,
): TransactionCookie<T>;
}
export interface TransactionCookie<T extends Record<string, unknown>> {
set(value: T): void;
consume(): T | undefined;
clear(): void;
}
export interface SessionStore {
@@ -259,7 +269,7 @@ export function createCookieStore(req: Request): CookieStore {
const incoming = parseCookieHeader(req.headers.get("cookie") ?? "");
const outgoing: string[] = [];
return {
const store: CookieStore = {
get(name) {
return incoming[name];
},
@@ -287,7 +297,45 @@ export function createCookieStore(req: Request): CookieStore {
headers() {
return [...outgoing];
},
transaction<T extends Record<string, unknown>>(name: string, options: CookieOptions = {}) {
const policy: CookieOptions = {
path: "/",
httpOnly: true,
sameSite: "Lax",
maxAge: 600,
secure: new URL(req.url).protocol === "https:",
...options,
};
return {
set(value: T) {
const json = JSON.stringify(value);
const encoded = btoa(unescape(encodeURIComponent(json)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
store.set(name, encoded, policy);
},
consume(): T | undefined {
const encoded = store.get(name);
store.delete(name, policy);
if (!encoded) return undefined;
try {
const padded = encoded
.replace(/-/g, "+")
.replace(/_/g, "/")
.padEnd(Math.ceil(encoded.length / 4) * 4, "=");
return JSON.parse(decodeURIComponent(escape(atob(padded)))) as T;
} catch {
return undefined;
}
},
clear() {
store.delete(name, policy);
},
};
},
};
return store;
}
export function createSessionStore(