import type { Context, Next } from "jsr:@hono/hono@4"; import { getCookie } from "jsr:@hono/hono@4/cookie"; import type { WSContext, WSEvents } from "jsr:@hono/hono@4/ws"; import type { AuthSdk, InvalidationHandler } from "./mod.ts"; /** * Universal Hono authentication middleware for Auth-Yes. * Extracts session tokens from cookies or Authorization Bearer headers, * validates against Auth-Yes, and injects 'userId' and 'scopes' into context. */ export function createAuthMiddleware(sdk: AuthSdk) { return async (c: Context, next: Next) => { let token = getCookie(c, "session_id"); if (!token) { const authHeader = c.req.header("Authorization"); if (authHeader && authHeader.startsWith("Bearer ")) { token = authHeader.substring("Bearer ".length); } } if (!token) { return c.json({ error: "Unauthorized: Missing session token" }, 401); } try { const session = await sdk.validateSession(token); if (!session.valid || !session.uuid) { return c.json( { error: session.error || "Unauthorized: Invalid session" }, 401, ); } c.set("userId", session.uuid); c.set("scopes", session.scopes || []); await next(); } catch (_e) { return c.json({ error: "Unauthorized" }, 401); } }; } /** * RBAC Scope Guard Middleware. * Ensures the authenticated user possesses the required application scope (or global 'admin'). */ export function requireScope(requiredScope: string) { return async (c: Context, next: Next) => { const scopes: string[] = c.get("scopes") || []; if (!scopes.includes(requiredScope) && !scopes.includes("admin")) { return c.json( { error: `Forbidden: Required scope '${requiredScope}' missing.` }, 403, ); } await next(); }; } /** * Creates a WebSocket session guard that integrates with the Ghost Cockpit Protocol. * This guard listens for `invalidate` events from the AuthSdk. If the monitored * session is invalidated, it sends an `AUTH_REVOKED` frame and closes the socket * with a 1008 policy violation. * * @example * ```typescript * import { upgradeWebSocket } from "jsr:@hono/hono/deno"; * import { getCookie } from "jsr:@hono/hono/cookie"; * * app.get("/ws", upgradeWebSocket((c) => { * const token = getCookie(c, "session_id"); * const guard = createWebSocketGuard(authSdk, token); * return { * ...guard, * onMessage(event, ws) { * // Handle regular application messages here * }, * }; * })); * ``` * * @param sdk The `AuthSdk` instance. * @param token The session token to monitor. If undefined, the guard will immediately close the socket upon connection. * @returns A partial `WSEvents` object containing `onOpen`, `onClose`, and `onError` handlers. */ export function createWebSocketGuard( sdk: AuthSdk, token?: string, ): Partial { let invalidationHandler: InvalidationHandler | undefined; return { onOpen(_event: Event, ws: WSContext) { if (!token) { // No session token provided, close immediately ws.send( JSON.stringify({ type: "AUTH_REVOKED", reason: "SESSION_EXPIRED" }), ); ws.close(1008, "Session Expired"); return; } invalidationHandler = (invalidatedToken: string) => { if (invalidatedToken === token) { try { ws.send( JSON.stringify({ type: "AUTH_REVOKED", reason: "SESSION_EXPIRED", }), ); } catch (_e) { // Ignore send errors if socket is already closing/closed } try { ws.close(1008, "Session Expired"); } catch (_e) { // Ignore close errors } } }; sdk.on("invalidate", invalidationHandler); }, onClose(_event: CloseEvent, _ws: WSContext) { if (invalidationHandler) { sdk.off("invalidate", invalidationHandler); } }, onError(_event: Event, _ws: WSContext) { if (invalidationHandler) { sdk.off("invalidate", invalidationHandler); } }, }; }