packages/wrap/src/middleware/auth/auth.controller.ts (contract + combine()) and middleware/auth/jwt-cookie.controller.ts (shipped preset). See also: Auth guide.
AuthController (abstract)
abstract class AuthController {
readonly [WRAP_AUTH_CONTROLLER]: true; // brand field — see "Branding" below
abstract authenticate(c: Context): Promise<AuthIdentity | null>;
abstract revoke(c: Context): Promise<void> | void;
abstract openApiSecurityScheme(): OpenApiSecuritySchemes;
authMiddleware: MiddlewareHandler<{ Variables: AppVariables }>;
guard(
predicate: (identity: AuthIdentity) => boolean,
onDenied?: (c: Context) => Response | Promise<Response>,
): MiddlewareHandler<{ Variables: AppVariables }>;
static combine(...controllers: AuthController[]): AuthController;
}
authenticate(c) — abstract, paradigm-specific
Resolve the identity behind this request, or null if unauthenticated. Verify a JWT, look up a session row, validate an API key, introspect an OAuth token — entirely up to the implementation. Free to constructor-inject a repository/service.
revoke(c) — abstract, paradigm-specific
Tear down whatever credential this request carries (logout): clear a cookie, delete a session row, revoke a token upstream. Should be a safe no-op when there’s nothing to clear (matters for combine() — see below).
openApiSecurityScheme() — abstract, paradigm-specific
type OpenApiSecurityScheme =
| { type: "http"; scheme: string; bearerFormat?: string; description?: string }
| { type: "apiKey"; in: "header" | "query" | "cookie"; name: string; description?: string }
| { type: "oauth2"; flows: OpenApiOAuthFlows; description?: string }
| { type: "openIdConnect"; openIdConnectUrl: string; description?: string };
type OpenApiSecuritySchemes = Record<string, OpenApiSecurityScheme>;
Declares the scheme(s) this strategy contributes to the generated OpenAPI spec (components.securitySchemes) — see Swagger. Mandatory: an un-overridden scheme used to silently produce empty/misleading docs, so there is no default implementation.
authMiddleware — generic, implemented once
authMiddleware: MiddlewareHandler<{ Variables: AppVariables }>
Calls authenticate(c); 401s if it returns null; otherwise c.set("identity", identity) and continues. Identical for every paradigm — not something a preset overrides. Apply it explicitly wherever a route group needs it: app.use("/admin/*", auth.authMiddleware). Internally tagged (WRAP_AUTH_MIDDLEWARE, a Symbol.for(...) global-registry symbol — resilient to duplicated module instances, unlike instanceof) so Swagger generation can detect a guarded route without sniffing the function’s name.
guard(predicate, onDenied?) — generic authorization
guard(
predicate: (identity: AuthIdentity) => boolean,
onDenied?: (c: Context) => Response | Promise<Response>,
): MiddlewareHandler
Reads c.get("identity") (set by authMiddleware, which must run first) and checks it against predicate. On failure, calls onDenied(c) if given, else responds 403. Paradigm-independent and role-agnostic by design — see Auth guide — no forced RBAC. A preset that wants role-based convenience builds it on top (JwtCookieAuthController.requireRoles, below) rather than the framework baking in a role field.
static combine(...controllers)
static combine(...controllers: AuthController[]): AuthController
Returns a CombinedAuthController (internal, not exported directly — always obtained via combine()):
authenticate(c)— tries each delegate’sauthenticate()in registration order; returns the first non-null identity, ornullif all delegates returnnull.revoke(c)— calls every delegate’srevoke()(best-effort; relies on each preset’s own safe-no-op contract).openApiSecurityScheme()—Object.assign({}, ...delegates.map(d => d.openApiSecurityScheme())), so the generated spec advertises every delegate’s scheme.
Use it to chain fallback paradigms in one app — e.g. cookie sessions for browsers, an API-key controller for clients that can’t store cookies. See Auth guide — combining paradigms.
Branding (WRAP_AUTH_CONTROLLER / WRAP_AUTH_MIDDLEWARE)
const WRAP_AUTH_CONTROLLER = Symbol.for("@donilite/wrap:authController");
const WRAP_AUTH_MIDDLEWARE = Symbol.for("@donilite/wrap:authMiddleware");
function isAuthController(value: unknown): value is AuthController;
Both use Symbol.for (the global symbol registry), not a module-scoped Symbol() — so the brand check still works if @donilite/wrap ends up duplicated across module instances (a monorepo with mismatched hoisting, a linked package, etc.), where instanceof would silently fail. Wrap.with() uses isAuthController() to decide whether an argument is an AuthController or a plain middleware — see Wrap.
JwtCookieAuthController extends AuthController
interface AuthOptions {
secret: string;
algorithm?: string; // default "HS256"
cookieName?: string; // default "session"
sessionMaxAgeSeconds?: number; // default 7 days
secureCookies?: boolean; // default false — set true in production (HTTPS)
}
class JwtCookieAuthController extends AuthController {
constructor(options: AuthOptions); // throws HTTPException(500) if options.secret is empty
authenticate(c: Context): Promise<AuthIdentity | null>;
revoke(c: Context): void; // deletes the session cookie
clearCookieSession(c: Context): void; // alias for revoke
requireRoles(allowed: readonly AppRoles[]): MiddlewareHandler;
setupCookieSession(
c: Context,
payload: JWTSessionBase,
): Promise<string>; // signs a JWT, sets it as the session cookie, `c.set("identity", payload)`; returns the token
openApiSecurityScheme(): OpenApiSecuritySchemes; // { bearerAuth: {...}, cookieAuth: {...} }
}
authenticate() checks the session cookie first, then falls back to an Authorization: Bearer header; verifies the JWT against options.secret/options.algorithm; on a cookie-based hit, re-signs and refreshes the cookie (sliding session — every authenticated request extends sessionMaxAgeSeconds).
const auth = new JwtCookieAuthController({ secret: process.env.JWT_SECRET!, secureCookies: isProd });
app.with(auth);
app.use("/admin/*", auth.authMiddleware);
// in a login handler:
await auth.setupCookieSession(c, { userId: user.id, role: user.role });
// in a logout handler:
auth.revoke(c);
requireRoles(allowed) is exactly this.guard((identity) => canAccess(identity.role, allowed)) — this preset’s own convenience on top of the generic guard(), not something every paradigm gets for free.
AuthIdentity
interface AuthIdentity extends Record<string, unknown> {}
Declared in registry.ts, deliberately unconstrained — no mandated role field. Augment it via declaration merging to get app-wide typing:
declare module "@donilite/wrap" {
interface WrapRegistry {
identity: { userId: string; role: UserRoles; tenantId: string };
}
}
See Registry.