import type { Context, Middleware } from "@wrnexus/core"; import type { AuthEngine } from "./engine.ts"; import { publicUser } from "./normalize.ts"; import type { AuthenticatedContext, AuthPublicUser, AuthSession } from "./types.ts"; export const AUTH_SESSION_KEY = "wrnexus.auth.session"; function wantsJson(ctx: Context): boolean { if (ctx.url.pathname.startsWith("/api/") || ctx.url.pathname.startsWith("/__wrnexus/")) return true; const accept = ctx.req.headers.get("accept") ?? ""; return accept.includes("application/json") && !accept.includes("text/html"); } export interface AuthSessionOptions { /** Optional shared SSO cookie containing an AuthEngine session id. */ cookieName?: string; } export function authSession(engine: AuthEngine, options: AuthSessionOptions = {}): Middleware { return async (ctx, next) => { const sessionId = (options.cookieName ? ctx.cookies.get(options.cookieName) : undefined) ?? ctx.session.get(AUTH_SESSION_KEY); if (!sessionId) { ctx.user = null; ctx.locals.authUser = null; delete ctx.locals.authSession; return next(); } const session = await engine.validateSession(sessionId); if (!session) { ctx.session.delete(AUTH_SESSION_KEY); ctx.user = null; ctx.locals.authUser = null; delete ctx.locals.authSession; return next(); } const user = await engine.getUser(session.userId); if (!user || user.status !== "active") { ctx.session.delete(AUTH_SESSION_KEY); ctx.user = null; ctx.locals.authUser = null; delete ctx.locals.authSession; return next(); } const safe = publicUser(user); ctx.user = safe; ctx.locals.authUser = safe; ctx.locals.authSession = session; return next(); }; } export function establishAuthSession( ctx: Context, session: AuthSession, user: AuthPublicUser, ): void { ctx.session.regenerate(); ctx.session.set(AUTH_SESSION_KEY, session.id); ctx.user = user; ctx.locals.authUser = user; ctx.locals.authSession = session; } export function clearAuthSession(ctx: Context): void { ctx.session.delete(AUTH_SESSION_KEY); ctx.user = null; delete ctx.locals.authUser; delete ctx.locals.authSession; } export function getAuthUser(ctx: Context): AuthPublicUser | null { return ( (ctx.locals.authUser as AuthPublicUser | null | undefined) ?? (ctx.user as AuthPublicUser | null | undefined) ?? null ); } /** Return a typed authenticated user or fail closed for direct handler use. */ export function requireAuthUser(ctx: Context): AuthPublicUser { const user = getAuthUser(ctx); if (!user) throw new AuthRequiredError(); return user; } export class AuthRequiredError extends Error { readonly code = "WRN-AUTH-REQUIRED"; readonly status = 401; constructor() { super("Authentication is required"); this.name = "AuthRequiredError"; } } export function getAuthSession(ctx: Context): AuthSession | null { return (ctx.locals.authSession as AuthSession | undefined) ?? null; } export interface RequireAuthOptions { loginPath?: string; returnToParam?: string; roles?: string[]; status?: AuthPublicUser["status"][]; } export function requireAuth(options: RequireAuthOptions = {}): Middleware { const loginPath = options.loginPath ?? "/sign-in"; const returnToParam = options.returnToParam ?? "returnTo"; return (ctx, next) => { const user = getAuthUser(ctx); const allowedStatus = options.status ?? ["active"]; const allowedRole = !options.roles?.length || options.roles.some((role) => user?.roles.includes(role)); if (user && allowedStatus.includes(user.status) && allowedRole) return next(); if (wantsJson(ctx)) { return Response.json( { ok: false, error: user ? "Forbidden" : "Unauthorized" }, { status: user ? 403 : 401 }, ); } const redirect = new URL(loginPath, ctx.url); redirect.searchParams.set(returnToParam, `${ctx.url.pathname}${ctx.url.search}`); return Response.redirect(redirect, 302); }; } export function isAuthenticatedContext(ctx: Context): ctx is AuthenticatedContext { return Boolean(getAuthUser(ctx)); }