UNPKG

@re-auth/http-adapters

Version:

HTTP adapters for ReAuth authentication framework

437 lines 17.5 kB
import { Hono } from "hono"; import { deleteCookie, getCookie, setCookie } from "hono/cookie"; import { createHttpAdapter, createRouteOverride, createCustomRoute, createAutoIntrospectionConfig, introspectReAuthEngine, findContextRules, createContextRule, OAuth2ContextRules, } from "../../utils/http-adapter-factory"; /** * Hono framework adapter implementation */ class HonoFrameworkAdapter { constructor(engine, app) { this.contextRules = []; this.adapterConfig = {}; this.app = app || new Hono(); this.engine = engine; } /** * Set the ReAuth engine instance (for shared adapter pattern) */ setEngine(engine) { this.engine = engine; } /** * Set context rules (for shared adapter pattern) */ setContextRules(rules) { this.contextRules = rules; } /** * Set adapter config (for shared adapter pattern) */ setAdapterConfig(config) { this.adapterConfig = config; } /** * Get expected inputs for a plugin step */ getExpectedInputs(pluginName, stepName) { return this.engine?.getStepInputs?.(pluginName, stepName) || []; } setupMiddleware(context) { // Store context rules and adapter config at instance level this.setContextRules(context.config.contextRules); this.setAdapterConfig({ cookieName: context.config.cookieName || "auth_token", cookieOptions: context.config.cookieOptions || {}, }); // Global middleware if (context.config.globalMiddleware) { for (const middleware of context.config.globalMiddleware) { this.app.use("*", middleware); } } // Auth middleware (much lighter without heavy context attachments) this.app.use("*", async (c, next) => { const token = this.extractToken(c); if (token) { try { const session = await context.engine.checkSession(token); if (session.valid && session.entity) { c.set("entity", session.entity); c.set("token", session.token); c.set("authenticated", true); } else { c.set("authenticated", false); c.set("entity", undefined); c.set("token", null); // clear cookie deleteCookie(c, this.adapterConfig.cookieName); } } catch (error) { console.warn("Invalid token:", error); } } if (!c.get("authenticated")) { c.set("authenticated", false); } await next(); }); } createRoute(method, path, handler, middleware = []) { const honoMethod = method.toLowerCase(); // No need to wrap handler for context attachment - everything is at adapter level now if (typeof this.app[honoMethod] === "function") { if (middleware.length > 0) { this.app[honoMethod](path, ...middleware, handler); } else { this.app[honoMethod](path, handler); } } } async extractInputs(context, pluginName, stepName) { // Get expected inputs from the shared engine instance instead of context const expectedInputs = this.getExpectedInputs?.(pluginName, stepName) || []; const inputs = {}; // Extract from body try { const body = await context.req.json(); // console.dir(body, { depth: null }); // console.log('expectedInputs', expectedInputs); expectedInputs.forEach((inputName) => { if (body && body[inputName] !== undefined) { inputs[inputName] = body[inputName]; } }); } catch { // Not JSON body, ignore } // Extract from query expectedInputs.forEach((inputName) => { const queryValue = context.req.query(inputName); if (queryValue !== undefined) { inputs[inputName] = queryValue; } }); // Extract from params expectedInputs.forEach((inputName) => { const paramValue = context.req.param(inputName); if (paramValue !== undefined) { inputs[inputName] = paramValue; } }); if (expectedInputs.includes("token")) { console.log("token", expectedInputs); // Token from cookie or header const token = this.extractToken(context); console.log("token", token); if (token) { inputs.token = token; } } return inputs; } /** * Add configurable context inputs from cookies and headers based on context rules */ addConfigurableContextInputs(context, inputs, pluginName, stepName, contextRules) { // Use stored context rules instead of parameter (for compatibility) const rulesToUse = contextRules.length > 0 ? contextRules : this.contextRules; // Find applicable context extraction rules const applicableRules = findContextRules(pluginName, stepName, rulesToUse); applicableRules.forEach((rule) => { // Extract cookies if (rule.extractCookies) { rule.extractCookies.forEach((cookieName) => { const cookieValue = getCookie(context, cookieName); if (cookieValue) { let value = cookieValue; // Apply transform if provided if (rule.transformInput) { value = rule.transformInput(cookieName, value, context); } inputs[cookieName] = value; } }); } // Extract headers if (rule.extractHeaders) { const headerConfig = rule.extractHeaders; if (Array.isArray(headerConfig)) { // Simple array format: ['header-name'] headerConfig.forEach((headerName) => { const headerValue = context.req.header(headerName); if (headerValue) { let value = headerValue; // Apply transform if provided if (rule.transformInput) { value = rule.transformInput(headerName, value, context); } inputs[headerName.replace(/-/g, "_")] = value; // Convert header-name to header_name } }); } else { // Object format: { 'header-name': 'inputName' } Object.entries(headerConfig).forEach(([headerName, inputName]) => { const headerValue = context.req.header(headerName); if (headerValue) { let value = headerValue; // Apply transform if provided if (rule.transformInput) { value = rule.transformInput(inputName, value, context); } inputs[inputName] = value; } }); } } }); } handleStepResponse(context, response, // Not used in Hono, context handles response result, httpConfig) { const { redirect, success, status, cookies, ...data } = result; // Handle token (set cookie) using stored adapter config if (data.token) { setCookie(context, this.adapterConfig.cookieName, data.token, this.adapterConfig.cookieOptions); } // Handle additional cookies from result.cookies if (cookies) { for (const [name, value] of Object.entries(cookies)) { setCookie(context, name, value); } } // Handle redirect if (redirect) { return context.redirect(redirect); } // Determine status code const statusCode = this.getStatusCode(result, httpConfig); // Send response return context.json({ success, ...data, }, statusCode); } /** * Handle configurable context outputs (set cookies and headers) based on context rules */ handleConfigurableContextOutputs(context, response, // Not used in Hono result, pluginName, stepName, contextRules) { // Use stored context rules instead of parameter (for compatibility) const rulesToUse = contextRules.length > 0 ? contextRules : this.contextRules; // Find applicable context extraction rules const applicableRules = findContextRules(pluginName, stepName, rulesToUse); for (const rule of applicableRules) { // Set cookies from result if (rule.setCookies) { for (const cookieName of rule.setCookies) { if (result[cookieName] !== undefined) { let value = result[cookieName]; // Apply transform if provided if (rule.transformOutput) { value = rule.transformOutput(cookieName, value, result, context); } // Handle complex cookie options if (typeof value === "object" && value !== null && "value" in value) { // Value is a cookie options object const { value: cookieValue, ...cookieOptions } = value; setCookie(context, cookieName, cookieValue, cookieOptions); } else { // Simple value setCookie(context, cookieName, value); } } } } // Set headers from result if (rule.setHeaders) { const headerConfig = rule.setHeaders; if (Array.isArray(headerConfig)) { // Simple array format: ['header-name'] for (const headerName of headerConfig) { const inputName = headerName.replace(/-/g, "_"); // Convert header-name to header_name if (result[inputName] !== undefined) { let value = result[inputName]; // Apply transform if provided if (rule.transformOutput) { value = rule.transformOutput(headerName, value, result, context); } context.header(headerName, value); } } } else { // Object format: { 'header-name': 'resultKey' } for (const [headerName, resultKey] of Object.entries(headerConfig)) { if (result[resultKey] !== undefined) { let value = result[resultKey]; // Apply transform if provided if (rule.transformOutput) { value = rule.transformOutput(headerName, value, result, context); } context.header(headerName, value); } } } } } } getStatusCode(result, httpConfig) { // First, check if the plugin step defines a specific status code for the result status if (result.status && httpConfig[result.status]) { return httpConfig[result.status]; } // Fallback to generic success/error codes from httpConfig if (result.success && httpConfig.success) { return httpConfig.success; } if (!result.success && httpConfig.error) { return httpConfig.error; } // Fallback to standard HTTP status codes based on result.status if (result.status === "redirect") return 302; if (result.status === "unauthorized") return 401; if (result.status === "forbidden") return 403; if (result.status === "not_found") return 404; if (result.status === "conflict") return 409; if (result.status === "error") return 400; // Default fallback return result.success ? 200 : 400; } extractToken(context) { // From cookie const cookieToken = getCookie(context, this.adapterConfig.cookieName); if (cookieToken) { return cookieToken; } // From Authorization header const authHeader = context.req.header("authorization"); if (authHeader?.startsWith("Bearer ")) { return authHeader.substring(7); } return null; } requireAuth() { return async (c, next) => { if (!c.get("authenticated")) { return c.json({ error: "Authentication required" }, 401); } await next(); }; } errorResponse(context, error) { console.error("HTTP Adapter Error:", error); return context.request.json({ success: false, message: error.message || "Internal server error", error: process.env.NODE_ENV !== "production" ? error : undefined, }, 500); } getAdapter() { return this.app; } } /** * Create Hono adapter using the factory pattern with shared instance */ export const createHonoAdapterV2 = (engine, config, frameworkAdapter) => { return createHttpAdapter(frameworkAdapter)(engine, config); }; /** * Hono adapter class with additional features */ export class HonoAdapterV2 { constructor(engine, config = {}, app, frameworkAdapter) { this.engine = engine; this.config = config; this.frameworkAdapter = frameworkAdapter || new HonoFrameworkAdapter(engine, app); // Set the engine on the shared adapter before creating the HTTP adapter this.frameworkAdapter.setEngine(engine); // Create the app using the factory createHonoAdapterV2(engine, config, this.frameworkAdapter); this.app = this.frameworkAdapter.getAdapter(); // No need for middleware to attach config - it's stored at adapter level now } /** * Get the Hono app */ getApp() { return this.app; } getAdapter() { return this.frameworkAdapter; } /** * Add a custom route */ addRoute(method, path, handler, options = {}) { const middleware = options.middleware || []; if (options.requireAuth) { middleware.push(this.frameworkAdapter.requireAuth()); } const honoMethod = method.toLowerCase(); if (typeof this.app[honoMethod] === "function") { if (middleware.length > 0) { this.app[honoMethod](path, ...middleware, handler); } else { this.app[honoMethod](path, handler); } } } /** * Protection middleware for routes */ protect(options = {}) { return async (c, next) => { // Check authentication if (!c.get("authenticated")) { return c.json({ error: "Authentication required" }, 401); } // Check roles if (options.roles && options.roles.length > 0) { const entity = c.get("entity"); const userRole = entity?.role; if (!userRole || !options.roles.includes(userRole)) { return c.json({ error: "Insufficient permissions" }, 403); } } // Custom authorization if (options.authorize) { try { const entity = c.get("entity"); const isAuthorized = await options.authorize(entity, c); if (!isAuthorized) { return c.json({ error: "Access denied" }, 403); } } catch (error) { console.error("Authorization error:", error); return c.json({ error: "Authorization check failed" }, 500); } } await next(); }; } } /** * Convenience function to create Hono adapter with options */ export function createHonoAdapter(engine, config = {}, app) { return new HonoAdapterV2(engine, config, app); } // Export utility functions export { createRouteOverride, createCustomRoute, createAutoIntrospectionConfig, introspectReAuthEngine, createContextRule, OAuth2ContextRules, HonoFrameworkAdapter, }; //# sourceMappingURL=hono-adapter-v2.js.map