@re-auth/http-adapters
Version:
HTTP adapters for ReAuth authentication framework
437 lines • 17.5 kB
JavaScript
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