import { getCookie, deleteCookie } from "hono/cookie"; import type { Context, Next } from "hono"; import { Agent } from "@atproto/api"; import type { AppContext } from "../lib/app-context.js"; import { restoreOAuthSession } from "../lib/session.js"; import type { AuthenticatedUser, Variables } from "../types.js"; /** * Helper to restore OAuth session from cookie and create an Agent. * * Delegates to the shared `restoreOAuthSession` for cookie lookup and * OAuth session restoration, then enriches the result with an Agent, * handle, and PDS URL to produce an AuthenticatedUser. * * Returns null if session doesn't exist or is expired (expected). * Throws on unexpected errors (network failures, etc.) that should bubble up. */ async function restoreSession(ctx: AppContext, cookieToken: string): Promise { const result = await restoreOAuthSession(ctx, cookieToken); if (!result) { return null; } const { oauthSession, cookieSession } = result; // Create Agent from OAuth session // The library's OAuthSession implements the fetch handler with DPoP const agent = new Agent(oauthSession); // Get handle from cookie session (fetched during login callback) // Fall back to DID if handle wasn't stored const handle = cookieSession.handle || oauthSession.did; const user: AuthenticatedUser = { did: oauthSession.did, handle, pdsUrl: oauthSession.serverMetadata.issuer, // PDS URL from server metadata agent, }; return user; } /** * Require authentication middleware. * * Validates session cookie and attaches authenticated user to context. * Returns 401 if session is missing or invalid. * * Usage: * app.post('/api/posts', requireAuth(ctx), async (c) => { * const user = c.get('user'); // Guaranteed to exist * const agent = user.agent; // Pre-configured Agent with DPoP * }); */ export function requireAuth(ctx: AppContext) { return async (c: Context<{ Variables: Variables }>, next: Next) => { const sessionToken = getCookie(c, "atbb_session"); if (!sessionToken) { return c.json({ error: "Authentication required" }, 401); } try { const user = await restoreSession(ctx, sessionToken); if (!user) { return c.json({ error: "Invalid or expired session" }, 401); } // Attach user to context c.set("user", user); await next(); } catch (error) { ctx.logger.error("Authentication middleware error", { path: c.req.path, error: error instanceof Error ? error.message : String(error), }); return c.json( { error: "Authentication failed. Please try again.", }, 500 ); } }; } /** * Optional authentication middleware. * * Validates session if present, but doesn't return 401 if missing. * Useful for endpoints that work for both authenticated and unauthenticated users. * * Usage: * app.get('/api/posts/:id', optionalAuth(ctx), async (c) => { * const user = c.get('user'); // May be undefined * if (user) { * // Show edit buttons, etc. * } * }); */ export function optionalAuth(ctx: AppContext) { return async (c: Context<{ Variables: Variables }>, next: Next) => { const sessionToken = getCookie(c, "atbb_session"); if (!sessionToken) { await next(); return; } try { const user = await restoreSession(ctx, sessionToken); if (user) { c.set("user", user); } else { // Session is invalid/expired - clean up the cookie deleteCookie(c, "atbb_session"); } } catch (error) { // restoreSession now throws on unexpected errors only // Log the unexpected error but don't fail the request ctx.logger.warn("Unexpected error during optional auth", { path: c.req.path, error: error instanceof Error ? error.message : String(error), }); // Clean up potentially corrupted cookie deleteCookie(c, "atbb_session"); } await next(); }; }