@re-auth/http-adapters
Version:
HTTP adapters for ReAuth authentication framework
336 lines • 13.1 kB
JavaScript
/**
* Introspect ReAuth engine and extract all available routes
*/
export function introspectReAuthEngine(engine, config = {}) {
const routes = [];
const plugins = engine.getAllPlugins();
const { includePlugins, excludePlugins = [], excludeSteps = [], pathGenerator = defaultPathGenerator, } = config;
for (const plugin of plugins) {
// Check if plugin should be included
if (includePlugins && !includePlugins.includes(plugin.name)) {
continue;
}
// Check if plugin should be excluded
if (excludePlugins.includes(plugin.name)) {
continue;
}
for (const step of plugin.steps) {
// Only include steps with HTTP protocol
if (!step.protocol?.http)
continue;
const stepKey = `${plugin.name}.${step.name}`;
// Check if step should be excluded
if (excludeSteps.includes(stepKey)) {
continue;
}
const basePath = "/auth"; // This will be updated in the factory
const autoRoute = {
pluginName: plugin.name,
stepName: step.name,
method: step.protocol.http.method,
path: pathGenerator(plugin.name, step.name, basePath),
requiresAuth: step.protocol.http.auth || false,
description: step.description,
inputs: step.inputs,
};
routes.push(autoRoute);
}
}
return routes;
}
/**
* Default path generator for auto-generated routes
*/
function defaultPathGenerator(pluginName, stepName, basePath) {
return `${basePath}/${pluginName}/${stepName}`;
}
/**
* Creates a generic HTTP adapter for any framework
*/
export function createHttpAdapter(frameworkAdapter) {
return (engine, config = {}) => {
// Merge with defaults
const mergedConfig = {
basePath: "/auth",
cookieName: "auth_token",
cookieOptions: {},
autoIntrospection: { enabled: true },
contextRules: [],
routeOverrides: [],
customRoutes: [],
globalMiddleware: [],
...config,
};
// Validate required environment variables for introspection
const requiredAuthKey = process.env.REAUTH_INTROSPECTION_KEY;
if (!requiredAuthKey) {
console.warn("REAUTH_INTROSPECTION_KEY environment variable not set. Introspection endpoint will be disabled.");
}
// Auto-introspect the engine
const autoGeneratedRoutes = mergedConfig.autoIntrospection.enabled
? introspectReAuthEngine(engine, {
...mergedConfig.autoIntrospection,
pathGenerator: mergedConfig.autoIntrospection.pathGenerator ||
defaultPathGenerator,
})
: [];
// Create adapter context
const context = {
engine,
config: mergedConfig,
extractToken: frameworkAdapter.extractToken.bind(frameworkAdapter),
requireAuth: frameworkAdapter.requireAuth.bind(frameworkAdapter),
handleStepResponse: frameworkAdapter.handleStepResponse.bind(frameworkAdapter),
errorResponse: frameworkAdapter.errorResponse.bind(frameworkAdapter),
autoGeneratedRoutes,
};
// Setup middleware
frameworkAdapter.setupMiddleware(context);
// Setup introspection endpoint first (before other routes)
if (requiredAuthKey) {
setupIntrospectionEndpoint(engine, frameworkAdapter, context, requiredAuthKey);
}
// Setup auto-generated routes
if (mergedConfig.autoIntrospection.enabled) {
setupAutoGeneratedRoutes(engine, frameworkAdapter, context);
}
// Setup custom routes
setupCustomRoutes(frameworkAdapter, context);
return frameworkAdapter.getAdapter();
};
}
/**
* Setup the introspection endpoint for SDK generation
*/
function setupIntrospectionEndpoint(engine, frameworkAdapter, context, requiredAuthKey) {
const introspectionHandler = async (request, response) => {
try {
// Validate auth key
const authKey = request.headers["x-auth-key"] || request.headers["x-reauth-key"];
if (!authKey || authKey !== requiredAuthKey) {
console.warn("Introspection attempt with invalid auth key:", authKey ? "provided" : "missing");
return frameworkAdapter.errorResponse(response, new Error("Invalid or missing auth key"));
}
// Get introspection data
const introspectionData = engine.getIntrospectionData();
// Send successful response
if (response.json) {
// Express-like response
response.status(200).json(introspectionData);
}
else if (response.send) {
// Fastify-like response
response.status(200).send(introspectionData);
}
else {
// Hono/Web framework-like response - return Response object
return new Response(JSON.stringify(introspectionData), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
}
catch (error) {
console.error("Introspection endpoint error:", error);
return frameworkAdapter.errorResponse(response, error);
}
};
// Create the introspection route
const introspectionPath = `${context.config.basePath}/introspect`;
frameworkAdapter.createRoute("GET", introspectionPath, introspectionHandler, []);
console.log(`✅ Introspection endpoint available at: GET ${introspectionPath}`);
console.log("🔑 Auth key required in header: x-auth-key or x-reauth-key");
}
/**
* Find applicable context extraction rules for a plugin/step combination
*/
export function findContextRules(pluginName, stepName, contextRules) {
return contextRules.filter((rule) => {
if (rule.pluginName !== pluginName)
return false;
if (rule.stepName && rule.stepName !== stepName)
return false;
return true;
});
}
/**
* Setup auto-generated routes from ReAuth engine introspection
*/
function setupAutoGeneratedRoutes(engine, frameworkAdapter, context) {
for (const autoRoute of context.autoGeneratedRoutes) {
// Check for route overrides
const override = context.config.routeOverrides.find((override) => override.pluginName === autoRoute.pluginName &&
override.stepName === autoRoute.stepName);
// Determine method, path, and handler
const method = override?.method || autoRoute.method;
const path = override?.path || autoRoute.path;
let handler;
if (override?.handler) {
// Use custom handler
handler = override.handler;
}
else {
// Create default handler with optional override hooks
handler = createAutoGeneratedStepHandler(autoRoute, frameworkAdapter, context, override);
}
// Determine middleware
const middleware = [
...(context.config.globalMiddleware || []),
...(override?.middleware || []),
];
// Add auth middleware if required
if (autoRoute.requiresAuth && !override?.handler) {
middleware.push(context.requireAuth());
}
// Create the route
frameworkAdapter.createRoute(method.toUpperCase(), path, handler, middleware);
}
}
/**
* Setup custom routes
*/
function setupCustomRoutes(frameworkAdapter, context) {
for (const route of context.config.customRoutes) {
const middleware = [
...(context.config.globalMiddleware || []),
...(route.middleware || []),
];
// Add auth middleware if required
if (route.requireAuth) {
middleware.push(context.requireAuth());
}
frameworkAdapter.createRoute(route.method, route.path, route.handler, middleware);
}
}
/**
* Create a step handler for auto-generated routes with override support</edit>
*/
function createAutoGeneratedStepHandler(autoRoute, frameworkAdapter, context, override) {
return async (request, response, next) => {
try {
// Get the actual step definition to access its HTTP protocol configuration
const plugin = context.engine.getPlugin(autoRoute.pluginName);
const step = plugin?.steps.find((s) => s.name === autoRoute.stepName);
const httpConfig = step?.protocol?.http || {};
// Extract inputs (with override support)
let inputs;
if (override?.extractInputs) {
inputs = await override.extractInputs(request, autoRoute.pluginName, autoRoute.stepName);
}
else {
inputs = await frameworkAdapter.extractInputs(request, autoRoute.pluginName, autoRoute.stepName);
}
// Add auth context if authenticated
if (request.user) {
inputs.entity = request.user;
inputs.token = request.token;
}
// Add configurable context inputs from cookies and headers
frameworkAdapter.addConfigurableContextInputs(request, inputs, autoRoute.pluginName, autoRoute.stepName, context.config.contextRules);
// Execute the step
const result = await context.engine.executeStep(autoRoute.pluginName, autoRoute.stepName, inputs);
// Handle the response (with override support)
if (override?.handleResponse) {
return override.handleResponse(request, response, result, httpConfig);
// biome-ignore lint/style/noUselessElse: <explanation>
}
else {
// Handle configurable context outputs (cookies and headers)
frameworkAdapter.handleConfigurableContextOutputs(request, response, result, autoRoute.pluginName, autoRoute.stepName, context.config.contextRules);
return context.handleStepResponse(request, response, result, httpConfig);
}
}
catch (error) {
console.error(`Error in ${autoRoute.pluginName}.${autoRoute.stepName}:`, error);
// Use custom error handler if provided
if (context.config.errorHandler) {
return context.config.errorHandler(error, {
request,
response,
pluginName: autoRoute.pluginName,
stepName: autoRoute.stepName,
});
}
return context.errorResponse({
request,
response,
pluginName: autoRoute.pluginName,
stepName: autoRoute.stepName,
}, error);
}
};
}
/**
* Utility function to create route override
*/
export function createRouteOverride(pluginName, stepName, override) {
return {
pluginName,
stepName,
...override,
};
}
/**
* Utility function to create custom route
*/
export function createCustomRoute(method, path, handler, options = {}) {
return {
method,
path,
handler,
middleware: options.middleware || [],
requireAuth: options.requireAuth || false,
};
}
/**
* Utility function to create auto-introspection config
*/
export function createAutoIntrospectionConfig(options = {}) {
return {
enabled: true,
...options,
};
}
/**
* Utility function to create context extraction rule
*/
export function createContextRule(pluginName, options = {}) {
return {
pluginName,
...options,
};
}
/**
* Predefined context rules for common OAuth scenarios
*/
export const OAuth2ContextRules = {
/**
* OAuth 2.0 with PKCE context rule for extracting state and code verifier
*/
pkce: (pluginName) => createContextRule(pluginName, {
extractCookies: ["oauth_state", "oauth_code_verifier"],
setCookies: ["oauth_state", "oauth_code_verifier"],
}),
/**
* OAuth 2.0 regular flow context rule for extracting state
*/
regular: (pluginName) => createContextRule(pluginName, {
extractCookies: ["oauth_state"],
setCookies: ["oauth_state"],
}),
/**
* OAuth callback context rule for all OAuth steps
*/
callback: (pluginName) => createContextRule(pluginName, {
stepName: "callback",
extractCookies: ["oauth_state", "oauth_code_verifier"],
}),
/**
* OAuth start context rule for setting state and code verifier
*/
start: (pluginName) => createContextRule(pluginName, {
stepName: "start",
setCookies: ["oauth_state", "oauth_code_verifier"],
}),
};
//# sourceMappingURL=http-adapter-factory.js.map