@re-auth/http-adapters
Version:
HTTP protocol adapters for ReAuth - framework-agnostic integration with Express, Fastify, and Hono
1,762 lines (1,754 loc) • 56.2 kB
JavaScript
'use strict';
var express = require('express');
var cookie = require('hono/cookie');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var express__default = /*#__PURE__*/_interopDefault(express);
// src/types.ts
var HttpAdapterError = class extends Error {
constructor(message, statusCode = 500, code = "INTERNAL_ERROR", details) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.details = details;
this.name = "HttpAdapterError";
}
};
var ValidationError = class extends HttpAdapterError {
constructor(message, details) {
super(message, 400, "VALIDATION_ERROR", details);
this.name = "ValidationError";
}
};
var AuthenticationError = class extends HttpAdapterError {
constructor(message, details) {
super(message, 401, "AUTHENTICATION_ERROR", details);
this.name = "AuthenticationError";
}
};
var AuthorizationError = class extends HttpAdapterError {
constructor(message, details) {
super(message, 403, "AUTHORIZATION_ERROR", details);
this.name = "AuthorizationError";
}
};
var NotFoundError = class extends HttpAdapterError {
constructor(message, details) {
super(message, 404, "NOT_FOUND", details);
this.name = "NotFoundError";
}
};
var RateLimitError = class extends HttpAdapterError {
constructor(message, details) {
super(message, 429, "RATE_LIMIT_EXCEEDED", details);
this.name = "RateLimitError";
}
};
// src/base-adapter.ts
var ReAuthHttpAdapter = class {
engine;
config;
endpoints = /* @__PURE__ */ new Map();
logger;
constructor(config, logger) {
this.engine = config.engine;
this.config = config;
this.logger = logger;
this.buildEndpoints();
}
/**
* Get adapter configuration
*/
getConfig() {
return this.config;
}
/**
* Build endpoint map from registered plugins
*/
buildEndpoints() {
const plugins = this.engine.getAllPlugins();
for (const plugin of plugins) {
if (!plugin.steps) continue;
for (const step of plugin.steps) {
const endpoint = {
pluginName: plugin.name,
stepName: step.name,
path: `${this.config.basePath || ""}/${plugin.name}/${step.name}`,
method: step.protocol?.http?.method || "POST",
requiresAuth: Boolean(step.protocol?.http?.auth),
description: step.description,
inputSchema: step.validationSchema,
outputSchema: step.outputs
};
const key = `${plugin.name}:${step.name}`;
this.endpoints.set(key, endpoint);
}
}
}
/**
* Get all available endpoints
*/
getEndpoints() {
return Array.from(this.endpoints.values());
}
/**
* Get endpoint by plugin and step name
*/
getEndpoint(pluginName, stepName) {
return this.endpoints.get(`${pluginName}:${stepName}`);
}
/**
* Execute authentication step
*/
async executeAuthStep(req, deviceInfo) {
const endpoint = this.getEndpoint(req.plugin.name, req.plugin.step);
if (!endpoint) {
throw new NotFoundError(
`Endpoint not found: ${req.plugin.name}/${req.plugin.step}`
);
}
if (endpoint.requiresAuth) {
await this.requireAuthentication(req, deviceInfo);
} else {
await this.getCurrentUser(req, deviceInfo);
}
const input = this.extractStepInput(req, endpoint, deviceInfo);
try {
const output = await this.engine.executeStep(
endpoint.pluginName,
endpoint.stepName,
input
);
const pl = this.engine.getPlugin(endpoint.pluginName);
const step = pl?.steps?.find((s) => s.name === endpoint.stepName);
const status = step?.protocol?.http?.codes?.[output.status] || 200;
return {
success: true,
data: {
...output,
sessionToken: output.token
},
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
},
status,
redirect: output.redirect,
// Include redirect URL for OAuth flow
secret: output.secret
// Include cookies to set for OAuth flow
};
} catch (error) {
throw new HttpAdapterError(
`Step execution failed: ${error instanceof Error ? error.message : String(error)}`,
500,
"STEP_EXECUTION_ERROR",
{ pluginName: endpoint.pluginName, stepName: endpoint.stepName, error }
);
}
}
/**
* Create a new session
*/
async createSession(req, deviceInfo) {
const { subjectType, subjectId, ttlSeconds } = req.body || {};
if (!subjectType || !subjectId) {
throw new ValidationError("subjectType and subjectId are required");
}
try {
const token = await this.engine.createSessionFor(
subjectType,
subjectId,
ttlSeconds,
deviceInfo
);
return {
success: true,
data: {
valid: true,
token,
expiresAt: ttlSeconds ? new Date(Date.now() + ttlSeconds * 1e3).toISOString() : void 0
},
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
};
} catch (error) {
throw new HttpAdapterError(
`Session creation failed: ${error instanceof Error ? error.message : String(error)}`,
500,
"SESSION_CREATION_ERROR"
);
}
}
/**
* Check session validity
*/
async checkSession(req, deviceInfo) {
const token = this.extractSessionToken(req);
if (!token) {
return {
success: false,
data: {
valid: false
},
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
};
}
try {
const result = await this.engine.checkSession(token, deviceInfo);
if (!result.subject) {
return {
success: false,
data: {
valid: false
},
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
};
}
return {
success: true,
data: {
valid: result.valid,
subject: result.subject,
token: result.token,
metadata: {
payload: result.payload,
type: result.type
}
},
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
};
} catch (error) {
return {
success: false,
data: {
valid: false
},
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
};
}
}
/**
* Destroy current session
*/
async destroySession(req) {
const token = this.extractSessionToken(req);
if (!token) {
throw new ValidationError("No session token provided");
}
try {
await this.engine.getSessionService().destroySession(token);
return {
success: true,
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
};
} catch (error) {
throw new HttpAdapterError(
`Session destruction failed: ${error instanceof Error ? error.message : String(error)}`,
500,
"SESSION_DESTRUCTION_ERROR"
);
}
}
/**
* List all available plugins
*/
async listPlugins() {
const plugins = this.engine.getAllPlugins();
const data = plugins.map((plugin) => ({
name: plugin.name,
description: `${plugin.name} authentication plugin`,
steps: (plugin.steps || []).map((step) => ({
name: step.name,
description: step.description,
method: step.protocol?.http?.method || "POST",
path: `${this.config.basePath || ""}/auth/${plugin.name}/${step.name}`,
requiresAuth: Boolean(step.protocol?.http?.auth)
}))
}));
return {
success: true,
data,
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
};
}
/**
* Get specific plugin details
*/
async getPlugin(pluginName) {
const plugin = this.engine.getPlugin(pluginName);
if (!plugin) {
throw new NotFoundError(`Plugin not found: ${pluginName}`);
}
const data = {
name: plugin.name,
description: `${plugin.name} authentication plugin`,
steps: (plugin.steps || []).map((step) => ({
name: step.name,
description: step.description,
method: step.protocol?.http?.method || "POST",
path: `${this.config.basePath || ""}/auth/${plugin.name}/${step.name}`,
requiresAuth: Boolean(step.protocol?.http?.auth),
inputs: step.inputs || [],
inputSchema: step.validationSchema?.toJsonSchema?.() || {},
outputSchema: step.outputs?.toJsonSchema?.() || {}
}))
};
return {
success: true,
data,
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
};
}
/**
* Get full introspection data
*/
async getIntrospection() {
const introspectionData = this.engine.getIntrospectionData();
const httpConfig = {
basePath: this.config.basePath || "",
tokenConfig: {
header: {
accessTokenHeader: this.config.headerToken?.accesssTokenHeader || "authorization",
refreshTokenHeader: this.config.headerToken?.refreshTokenHeader || "x-refresh-token",
useBearer: true
// Default to Bearer token in Authorization header
},
cookie: {
accessTokenName: this.config.cookie?.name || "reauth-session",
refreshTokenName: this.config.cookie?.refreshTokenName || "reauth-refresh",
enabled: !!this.config.cookie
}
}
};
return {
success: true,
data: {
...introspectionData,
httpConfig
},
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
};
}
/**
* Health check endpoint
*/
async healthCheck() {
return {
success: true,
data: {
status: "healthy",
version: "2.0.0",
plugins: this.engine.getAllPlugins().length,
endpoints: this.endpoints.size
},
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
};
}
/**
* Extract session token from request
*/
extractSessionToken(req) {
let accessToken = null;
let refreshToken = null;
const authHeader = req.headers.authorization;
if (authHeader?.startsWith("Bearer ")) {
accessToken = authHeader.substring(7);
}
if (!accessToken && req.headers[this.config.headerToken?.accesssTokenHeader || "x-access-token"]) {
accessToken = req.headers[this.config.headerToken?.accesssTokenHeader || "x-access-token"];
}
if (!refreshToken && req.headers[this.config.headerToken?.refreshTokenHeader || "x-refresh-token"]) {
refreshToken = req.headers[this.config.headerToken?.refreshTokenHeader || "x-refresh-token"];
}
if (!accessToken && req.cookies?.[this.config.cookie?.name || "reauth-session"]) {
accessToken = req.cookies[this.config.cookie?.name || "reauth-session"];
}
if (!refreshToken && req.cookies?.[this.config.cookie?.refreshTokenName || "reauth-refresh"]) {
refreshToken = req.cookies[this.config.cookie?.refreshTokenName || "reauth-refresh"];
}
if (!accessToken) {
this.logger.info("http", "No access token found in request", {
path: req.path,
hasHeaders: !!req.headers
});
return null;
}
if (accessToken && refreshToken) {
this.logger.info("http", "Access token and refresh token found", {
path: req.path,
hasHeaders: !!req.headers
});
return { accessToken, refreshToken };
}
if (accessToken && !refreshToken) {
this.logger.info("http", "Access token found without refresh token", {
path: req.path,
hasHeaders: !!req.headers
});
return accessToken;
}
this.logger.info("http", "No access token or refresh token found", {
path: req.path,
hasHeaders: !!req.headers
});
return null;
}
/**
* Extract step input from request
*/
extractStepInput(req, endpoint, deviceInfo) {
const input = {
...req.body,
...req.query,
...req.params,
...req.cookies
// Add cookies so OAuth flow can read oauth_state, etc.
};
const token = this.extractSessionToken(req);
if (token) {
input.token = token;
}
if (deviceInfo) {
input.deviceInfo = deviceInfo;
}
if (req.ip) {
input.ip = req.ip;
}
if (req.userAgent) {
input.userAgent = req.userAgent;
}
return input;
}
/**
* Get current authenticated user from request
*/
async getCurrentUser(req, deviceInfo) {
const token = this.extractSessionToken(req);
if (!token) {
this.logger.info("http", "No token found in getCurrentUser", {
path: req.path,
hasHeaders: !!req.headers
});
return null;
}
try {
const session = await this.engine.checkSession(token, deviceInfo);
if (!session.valid || !session.subject) {
this.logger.warn("http", "Session is not valid or subject not found", {
hasHeaders: !!req.headers
});
return null;
}
return {
subject: session.subject,
token: session.token || token,
valid: session.valid,
metadata: {
payload: session.payload,
type: session.type
}
};
} catch (error) {
this.logger.error("http", "Session check failed", {
hasHeaders: !!req.headers,
error
});
return null;
}
}
/**
* Authenticate request and return user or throw error
*/
async requireAuthentication(req, deviceInfo) {
const user = await this.getCurrentUser(req, deviceInfo);
if (!user) {
this.logger.warn("http", "Authentication required but no user found", {
path: req.path,
hasHeaders: !!req.headers
});
throw new AuthenticationError("Authentication required");
}
return user;
}
/**
* Create framework-specific adapter
*/
createAdapter(adapter) {
return adapter;
}
/**
* Generic route handler factory
*/
createRouteHandler(handler) {
return async (req, res) => {
try {
const result = await handler(req, res);
if (result) {
res.status(200).json(result);
}
} catch (error) {
this.handleError(error, res);
}
};
}
/**
* Error handler
*/
handleError(error, res) {
if (error instanceof HttpAdapterError) {
res.status(error.statusCode).json({
success: false,
error: {
code: error.code,
message: error.message,
details: error.details
},
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
});
} else {
res.status(500).json({
success: false,
error: {
code: "INTERNAL_ERROR",
message: "An unexpected error occurred"
},
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
});
}
}
};
var ExpressAdapter = class {
constructor(config, exposeIntrospection, generateDeviceInfo, logger) {
this.exposeIntrospection = exposeIntrospection;
const defaultLogger = {
info: () => {
},
warn: () => {
},
error: () => {
},
success: () => {
},
setEnabledTags: () => {
},
destroy: () => {
}
};
this.adapter = new ReAuthHttpAdapter(config, logger || defaultLogger);
this.generateDeviceInfo = generateDeviceInfo;
}
name = "express";
adapter;
generateDeviceInfo;
/**
* Generate device info from request
*/
async generateDeviceInfoInternal(request) {
if (!this.generateDeviceInfo) {
return {};
}
const deviceInfo = await this.generateDeviceInfo(request);
return deviceInfo;
}
/**
* Create Express middleware that populates req.user
*/
createUserMiddleware() {
return async (req, res, next) => {
try {
const httpReq = this.extractRequest(req);
const deviceInfo = await this.generateDeviceInfoInternal(req);
req.user = await this.adapter.getCurrentUser(
httpReq,
deviceInfo
);
next();
} catch (error) {
req.user = null;
next();
}
};
}
/**
* Create Express middleware that adds adapter to request
*/
createMiddleware() {
return async (req, res, next) => {
req.reauth = this.adapter;
next();
};
}
/**
* Extract HTTP request from Express request
*/
extractRequest(req) {
return {
method: req.method,
url: req.url,
path: req.path,
query: req.query,
params: req.params,
body: req.body,
headers: req.headers,
cookies: req.cookies,
ip: req.ip,
userAgent: req.get("User-Agent")
};
}
/**
* Send response using Express response object
*/
sendResponse(res, data, statusCode = 200) {
res.status(statusCode).json(data);
}
/**
* Handle error using Express response object
*/
handleError(res, error, statusCode = 500) {
res.status(statusCode).json({
success: false,
error: {
code: "ERROR",
message: error.message
},
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
});
}
/**
* Create Express router with all ReAuth routes
*/
createRouter(router, basePath = "") {
router = router || express__default.default.Router();
router.use(this.createMiddleware());
router.get(`${basePath}/session`, this.createSessionCheckHandler());
if (this.exposeIntrospection) {
router.get(
`${basePath}/introspection`,
this.createIntrospectionHandler()
);
router.get(`${basePath}/plugins`, this.createPluginListHandler());
router.get(
`${basePath}/plugins/:plugin`,
this.createPluginDetailsHandler()
);
}
router.get(`${basePath}/health`, this.createHealthHandler());
const endpoints = this.adapter.getEndpoints();
for (const endpoint of endpoints) {
if (endpoint.method === "POST") {
router.post(
endpoint.path,
(req, res) => this.createStepHandler({
name: endpoint.pluginName,
step: endpoint.stepName
})
);
} else if (endpoint.method === "GET") {
router.get(
endpoint.path,
this.createStepHandler({
name: endpoint.pluginName,
step: endpoint.stepName
})
);
} else if (endpoint.method === "PUT") {
router.put(
endpoint.path,
this.createStepHandler({
name: endpoint.pluginName,
step: endpoint.stepName
})
);
} else if (endpoint.method === "PATCH") {
router.patch(
endpoint.path,
this.createStepHandler({
name: endpoint.pluginName,
step: endpoint.stepName
})
);
} else if (endpoint.method === "DELETE") {
router.delete(
endpoint.path,
this.createStepHandler({
name: endpoint.pluginName,
step: endpoint.stepName
})
);
}
}
return router;
}
/**
* Create step execution handler
*/
createStepHandler(plugin) {
return async (req, res) => {
try {
const httpReq = this.extractRequest(req);
const request = {
...httpReq,
plugin: {
name: plugin.name,
step: plugin.step
}
};
const deviceInfo = await this.generateDeviceInfoInternal(req);
const result = await this.adapter.executeAuthStep(request, deviceInfo);
if (result.secret && typeof result.secret === "object") {
for (const [key, value] of Object.entries(result.secret)) {
res.cookie(key, value, {
httpOnly: true,
secure: true,
path: "/",
sameSite: "lax",
maxAge: 6e5
// 10 minutes for OAuth state/verifier
});
}
}
if (this.adapter.getConfig().cookie && result.data?.token) {
const cookie = this.adapter.getConfig().cookie;
const options = cookie.options;
const token = result.data.token;
if (typeof token === "string") {
res.cookie(cookie.name, token, {
domain: options.domain,
httpOnly: options.httpOnly,
secure: options.secure,
path: options.path || "/",
sameSite: options.sameSite || "lax",
maxAge: options.maxAge ? options.maxAge * 1e3 : void 0,
expires: options.expires
});
} else {
res.cookie(cookie.name, token.accessToken, {
domain: options.domain,
httpOnly: options.httpOnly,
secure: options.secure,
path: options.path || "/",
sameSite: options.sameSite || "lax",
maxAge: options.maxAge ? options.maxAge * 1e3 : void 0,
expires: options.expires
});
if (cookie.refreshOptions && token.refreshToken) {
const refreshOptions = cookie.refreshOptions;
res.cookie(
cookie.refreshTokenName || "refreshToken",
token.refreshToken,
{
domain: refreshOptions.domain,
httpOnly: refreshOptions.httpOnly,
secure: refreshOptions.secure,
path: refreshOptions.path || "/",
sameSite: refreshOptions.sameSite || "lax",
maxAge: refreshOptions.maxAge ? refreshOptions.maxAge * 1e3 : void 0,
expires: refreshOptions.expires
}
);
}
}
}
if (result.redirect) {
res.redirect(result.status, result.redirect);
return;
}
this.sendResponse(res, result, result.status);
} catch (error) {
this.handleError(res, error);
}
};
}
/**
* Create session check handler
*/
createSessionCheckHandler() {
return async (req, res) => {
try {
const httpReq = this.extractRequest(req);
const deviceInfo = await this.generateDeviceInfoInternal(req);
const result = await this.adapter.checkSession(
httpReq,
deviceInfo
);
this.sendResponse(res, result);
} catch (error) {
this.handleError(res, error);
}
};
}
/**
* Create plugin list handler
*/
createPluginListHandler() {
return async (req, res) => {
try {
const result = await this.adapter.listPlugins();
this.sendResponse(res, result);
} catch (error) {
this.handleError(res, error);
}
};
}
/**
* Create plugin details handler
*/
createPluginDetailsHandler() {
return async (req, res) => {
try {
const { plugin } = req.params;
if (!plugin) {
this.handleError(res, new Error("Plugin parameter is required"), 400);
return;
}
const result = await this.adapter.getPlugin(plugin);
this.sendResponse(res, result);
} catch (error) {
this.handleError(res, error);
}
};
}
/**
* Create introspection handler
*/
createIntrospectionHandler() {
return async (req, res) => {
try {
const result = await this.adapter.getIntrospection();
this.sendResponse(res, result);
} catch (error) {
this.handleError(res, error);
}
};
}
/**
* Create health check handler
*/
createHealthHandler() {
return async (req, res) => {
try {
const result = await this.adapter.healthCheck();
this.sendResponse(res, result);
} catch (error) {
this.handleError(res, error);
}
};
}
/**
* Get current user from Express request
*/
async getCurrentUser(req) {
if (req.user !== void 0) {
return req.user;
}
const httpReq = this.extractRequest(req);
const deviceInfo = await this.generateDeviceInfoInternal(req);
return await this.adapter.getCurrentUser(httpReq, deviceInfo);
}
/**
* Get the base adapter instance
*/
getAdapter() {
return this.adapter;
}
};
function createExpressAdapter(config, exposeIntrospection = false, generateDeviceInfo, logger) {
return new ExpressAdapter(
config,
exposeIntrospection,
generateDeviceInfo,
logger
);
}
function expressReAuth(config, exposeIntrospection = false, generateDeviceInfo, logger) {
const adapter = new ExpressAdapter(
config,
exposeIntrospection,
generateDeviceInfo,
logger
);
return adapter.createMiddleware();
}
var HonoAdapter = class {
name = "hono";
adapter;
generateDeviceInfo;
constructor(config, generateDeviceInfo, logger) {
const defaultLogger = {
info: () => {
},
warn: () => {
},
error: () => {
},
success: () => {
},
setEnabledTags: () => {
},
destroy: () => {
}
};
this.adapter = new ReAuthHttpAdapter(config, logger || defaultLogger);
this.generateDeviceInfo = generateDeviceInfo;
}
/**
* Generate device info from request
*/
async generateDeviceInfoInternal(request) {
if (!this.generateDeviceInfo) {
return {};
}
const deviceInfo = await this.generateDeviceInfo(request);
return deviceInfo;
}
/**
* Create Hono user middleware that populates c.get('user')
*/
createUserMiddleware() {
return async (c, next) => {
try {
const httpReq = this.extractRequest(c);
const deviceInfo = await this.generateDeviceInfoInternal(c);
const user = await this.adapter.getCurrentUser(httpReq, deviceInfo);
c.set("user", user);
c.set("authenticated", !!user);
} catch (error) {
c.set("user", null);
c.set("authenticated", false);
}
await next();
};
}
/**
* Get current user from Hono context
*/
async getCurrentUser(c) {
const user = c.get("user");
if (user !== void 0) {
return user;
}
const httpReq = this.extractRequest(c);
const deviceInfo = await this.generateDeviceInfoInternal(c);
const u = await this.adapter.getCurrentUser(httpReq, deviceInfo);
c.set("user", u);
c.set("authenticated", !!u);
return u;
}
/**
* Extract HTTP request from Hono context
*/
extractRequest(c) {
const url = new URL(c.req.url);
return {
method: c.req.method,
url: c.req.url,
path: url.pathname,
query: Object.fromEntries(url.searchParams.entries()),
params: c.req.param(),
body: {},
headers: c.req.raw.headers ? Object.fromEntries(c.req.raw.headers.entries()) : {},
cookies: {},
ip: c.env?.CF_CONNECTING_IP || c.req.header("x-forwarded-for") || c.req.header("x-real-ip"),
userAgent: c.req.header("user-agent")
};
}
/**
* Send response using Hono context
*/
sendResponse(c, data, statusCode = 200) {
c.status(statusCode);
c.json(data);
}
/**
* Handle error using Hono context
*/
handleError(c, error, statusCode = 500) {
c.status(statusCode);
c.json({
success: false,
error: {
code: "ERROR",
message: error.message
},
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
});
}
/**
* Register routes on existing Hono app
*/
registerRoutes(app, basePath = "", exposeIntrospection = false) {
if (exposeIntrospection) {
app.get(`${basePath}/plugins`, this.createPluginListHandler());
app.get(`${basePath}/plugins/:plugin`, this.createPluginDetailsHandler());
app.get(`${basePath}/introspection`, this.createIntrospectionHandler());
}
app.get(`${basePath}/health`, this.createHealthHandler());
app.get(`${basePath}/session`, this.createSessionCheckHandler());
const endpoints = this.adapter.getEndpoints();
for (const endpoint of endpoints) {
if (endpoint.method === "POST") {
app.post(
`${basePath}${endpoint.path}`,
this.createStepHandler(endpoint.pluginName, endpoint.stepName)
);
} else if (endpoint.method === "GET") {
app.get(
`${basePath}${endpoint.path}`,
this.createStepHandler(endpoint.pluginName, endpoint.stepName)
);
} else if (endpoint.method === "PUT") {
app.put(
`${basePath}${endpoint.path}`,
this.createStepHandler(endpoint.pluginName, endpoint.stepName)
);
} else if (endpoint.method === "PATCH") {
app.patch(
`${basePath}${endpoint.path}`,
this.createStepHandler(endpoint.pluginName, endpoint.stepName)
);
} else if (endpoint.method === "DELETE") {
app.delete(
`${basePath}${endpoint.path}`,
this.createStepHandler(endpoint.pluginName, endpoint.stepName)
);
}
}
}
/**
* Create step execution handler
*/
createStepHandler(pluginName, stepName) {
return async (c) => {
try {
const httpReq = this.extractRequest(c);
httpReq;
const cookieHeader = c.req.header("cookie");
if (cookieHeader) {
const cookies = {};
cookieHeader.split(";").forEach((cookie) => {
const parts = cookie.split("=");
const key = parts.shift()?.trim();
const value = decodeURIComponent(parts.join("="));
if (key) {
cookies[key] = value;
}
});
httpReq.cookies = cookies;
}
if (["POST", "PUT", "PATCH"].includes(c.req.method)) {
try {
httpReq.body = await c.req.json();
} catch {
httpReq.body = {};
}
}
const req = {
...httpReq,
plugin: {
name: pluginName,
step: stepName
}
};
const deviceInfo = await this.generateDeviceInfoInternal(c);
const result = await this.adapter.executeAuthStep(req, deviceInfo);
if (result.secret && typeof result.secret === "object") {
for (const [key, value] of Object.entries(result.secret)) {
cookie.setCookie(c, key, value, {
httpOnly: true,
secure: true,
path: "/",
sameSite: "Lax",
maxAge: 600
// 10 minutes for OAuth state/verifier
});
}
}
if (this.adapter.getConfig().cookie) {
const cookie$1 = this.adapter.getConfig().cookie;
const options = cookie$1.options;
if (result.data?.token) {
const token = result.data.token;
if (typeof token === "string") {
cookie.setCookie(c, cookie$1.name, token, {
domain: options.domain,
httpOnly: options.httpOnly,
secure: options.secure,
path: options.path || "/",
sameSite: options.sameSite || "Lax",
partitioned: options.partitioned,
maxAge: options.maxAge,
expires: options.expires,
prefix: options.prefix,
priority: options.priority
});
} else {
cookie.setCookie(c, cookie$1.name, token.accessToken, {
domain: options.domain,
httpOnly: options.httpOnly,
secure: options.secure,
path: options.path || "/",
sameSite: options.sameSite || "Lax",
partitioned: options.partitioned,
maxAge: options.maxAge,
expires: options.expires,
prefix: options.prefix,
priority: options.priority
});
if (cookie$1.refreshOptions && token.refreshToken) {
const refreshOptions = cookie$1.refreshOptions;
cookie.setCookie(
c,
cookie$1.refreshTokenName || "refreshToken",
token.refreshToken,
{
domain: refreshOptions.domain,
httpOnly: refreshOptions.httpOnly,
secure: refreshOptions.secure,
path: refreshOptions.path || "/",
sameSite: refreshOptions.sameSite || "Lax",
partitioned: refreshOptions.partitioned,
maxAge: refreshOptions.maxAge,
expires: refreshOptions.expires,
prefix: refreshOptions.prefix,
priority: refreshOptions.priority
}
);
}
}
}
}
if (result.redirect) {
return c.redirect(result.redirect, result.status);
}
return c.json(result, result.status);
} catch (error) {
return this.handleErrorResponse(c, error);
}
};
}
createSessionCheckHandler() {
return async (c) => {
try {
const httpReq = this.extractRequest(c);
const deviceInfo = await this.generateDeviceInfoInternal(c);
const result = await this.adapter.checkSession(
httpReq,
deviceInfo
);
return c.json(result);
} catch (error) {
return this.handleErrorResponse(c, error);
}
};
}
/**
* Create plugin list handler
*/
createPluginListHandler() {
return async (c) => {
try {
const result = await this.adapter.listPlugins();
return c.json(result);
} catch (error) {
return this.handleErrorResponse(c, error);
}
};
}
/**
* Create plugin details handler
*/
createPluginDetailsHandler() {
return async (c) => {
try {
const plugin = c.req.param("plugin");
const result = await this.adapter.getPlugin(plugin);
return c.json(result);
} catch (error) {
return this.handleErrorResponse(c, error);
}
};
}
/**
* Create introspection handler
*/
createIntrospectionHandler() {
return async (c) => {
try {
const result = await this.adapter.getIntrospection();
return c.json(result);
} catch (error) {
return this.handleErrorResponse(c, error);
}
};
}
/**
* Create health check handler
*/
createHealthHandler() {
return async (c) => {
try {
const result = await this.adapter.healthCheck();
return c.json(result);
} catch (error) {
return this.handleErrorResponse(c, error);
}
};
}
/**
* Handle error response
*/
handleErrorResponse(c, error) {
c.status(500);
return c.json({
success: false,
error: {
code: "ERROR",
message: error.message
},
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
});
}
/**
* Get the base adapter instance
*/
getAdapter() {
return this.adapter;
}
};
function honoReAuth(config, generateDeviceInfo, logger) {
const adapter = new HonoAdapter(config, generateDeviceInfo, logger);
return adapter;
}
// src/middleware/security.ts
function createCorsMiddleware(config = {}) {
const {
origin = "*",
credentials = false,
allowedHeaders = ["Content-Type", "Authorization"],
exposedHeaders = [],
methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
preflightContinue = false,
optionsSuccessStatus = 204
} = config;
return (req, res, next) => {
if (typeof origin === "string") {
res.header("Access-Control-Allow-Origin", origin);
} else if (typeof origin === "boolean") {
res.header("Access-Control-Allow-Origin", origin ? "*" : "false");
} else if (Array.isArray(origin)) {
const requestOrigin = req.headers.origin;
if (requestOrigin && origin.includes(requestOrigin)) {
res.header("Access-Control-Allow-Origin", requestOrigin);
}
} else if (typeof origin === "function") {
const requestOrigin = req.headers.origin;
origin(requestOrigin, (err, allow) => {
if (err) {
return next(err);
}
if (allow) {
res.header("Access-Control-Allow-Origin", requestOrigin || "*");
}
});
}
if (credentials) {
res.header("Access-Control-Allow-Credentials", "true");
}
res.header("Access-Control-Allow-Methods", methods.join(", "));
if (allowedHeaders.length > 0) {
res.header("Access-Control-Allow-Headers", allowedHeaders.join(", "));
}
if (exposedHeaders.length > 0) {
res.header("Access-Control-Expose-Headers", exposedHeaders.join(", "));
}
if (req.method === "OPTIONS") {
if (!preflightContinue) {
res.status(optionsSuccessStatus).end();
return;
}
}
next();
};
}
function createRateLimitMiddleware(config = {}) {
const {
windowMs = 15 * 60 * 1e3,
// 15 minutes
max = 100,
message = "Too many requests, please try again later.",
standardHeaders = true,
legacyHeaders = false,
skipSuccessfulRequests = false,
skipFailedRequests = false,
keyGenerator = (req) => req.ip || "anonymous"
} = config;
const requests = /* @__PURE__ */ new Map();
return (req, res, next) => {
const key = keyGenerator(req);
const now = Date.now();
const resetTime = now + windowMs;
for (const [k, v] of requests.entries()) {
if (v.resetTime <= now) {
requests.delete(k);
}
}
let requestInfo = requests.get(key);
if (!requestInfo || requestInfo.resetTime <= now) {
requestInfo = { count: 0, resetTime };
requests.set(key, requestInfo);
}
if (requestInfo.count >= max) {
if (standardHeaders) {
res.header("X-RateLimit-Limit", max.toString());
res.header("X-RateLimit-Remaining", "0");
res.header(
"X-RateLimit-Reset",
new Date(requestInfo.resetTime).toISOString()
);
}
if (legacyHeaders) {
res.header("X-Rate-Limit-Limit", max.toString());
res.header("X-Rate-Limit-Remaining", "0");
res.header(
"X-Rate-Limit-Reset",
new Date(requestInfo.resetTime).toISOString()
);
}
return res.status(429).json({
success: false,
error: {
code: "RATE_LIMIT_EXCEEDED",
message
},
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
});
}
requestInfo.count++;
if (standardHeaders) {
res.header("X-RateLimit-Limit", max.toString());
res.header("X-RateLimit-Remaining", (max - requestInfo.count).toString());
res.header(
"X-RateLimit-Reset",
new Date(requestInfo.resetTime).toISOString()
);
}
if (legacyHeaders) {
res.header("X-Rate-Limit-Limit", max.toString());
res.header(
"X-Rate-Limit-Remaining",
(max - requestInfo.count).toString()
);
res.header(
"X-Rate-Limit-Reset",
new Date(requestInfo.resetTime).toISOString()
);
}
next();
};
}
function createSecurityMiddleware(config = {}) {
const { helmet = true } = config;
return (req, res, next) => {
if (helmet) {
res.header("X-Content-Type-Options", "nosniff");
res.header("X-Frame-Options", "DENY");
res.header("X-XSS-Protection", "1; mode=block");
res.header("Referrer-Policy", "strict-origin-when-cross-origin");
res.header(
"Permissions-Policy",
"geolocation=(), microphone=(), camera=()"
);
if (req.secure || req.headers["x-forwarded-proto"] === "https") {
res.header(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains"
);
}
}
next();
};
}
function createValidationMiddleware(config = {}) {
const {
validateInput = true,
maxPayloadSize = 1024 * 1024,
// 1MB
allowedFields = [],
sanitizeFields = ["email", "username", "name"]
} = config;
return (req, res, next) => {
if (!validateInput) {
return next();
}
const contentLength = parseInt(req.headers["content-length"] || "0", 10);
if (contentLength > maxPayloadSize) {
return res.status(413).json({
success: false,
error: {
code: "PAYLOAD_TOO_LARGE",
message: `Payload too large. Maximum size is ${maxPayloadSize} bytes.`
},
meta: {
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
});
}
if (req.body && typeof req.body === "object") {
for (const field of sanitizeFields) {
if (req.body[field] && typeof req.body[field] === "string") {
req.body[field] = req.body[field].replace(/<[^>]*>/g, "").trim();
}
}
if (allowedFields.length > 0) {
const filtered = {};
for (const field of allowedFields) {
if (req.body[field] !== void 0) {
filtered[field] = req.body[field];
}
}
req.body = filtered;
}
}
next();
};
}
function createSecurityMiddlewares(config) {
const middlewares = [];
if (config.security) {
middlewares.push(createSecurityMiddleware(config.security));
}
if (config.cors) {
middlewares.push(createCorsMiddleware(config.cors));
}
if (config.rateLimit) {
middlewares.push(createRateLimitMiddleware(config.rateLimit));
}
if (config.validation) {
middlewares.push(createValidationMiddleware(config.validation));
}
return middlewares;
}
// src/utils/factory.ts
function createReAuthHttpAdapter(config, logger) {
const defaultConfig = {
basePath: "/api/v2",
cors: {
origin: true,
credentials: true,
allowedHeaders: ["Content-Type", "Authorization"],
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]
},
rateLimit: {
windowMs: 15 * 60 * 1e3,
// 15 minutes
max: 100,
message: "Too many requests, please try again later."
},
security: {
helmet: true,
sanitizeInput: true,
sanitizeOutput: false
},
validation: {
validateInput: true,
maxPayloadSize: 1024 * 1024,
// 1MB
sanitizeFields: ["email", "username", "name", "description"]
},
...config
};
return new ReAuthHttpAdapter(defaultConfig, logger);
}
function generateApiSpec(adapter) {
const endpoints = adapter.getEndpoints();
const paths = {};
for (const endpoint of endpoints) {
const path = endpoint.path;
const method = endpoint.method.toLowerCase();
if (!paths[path]) {
paths[path] = {};
}
paths[path][method] = {
summary: `Execute ${endpoint.stepName} step for ${endpoint.pluginName} plugin`,
description: endpoint.description || `Execute authentication step`,
tags: [endpoint.pluginName],
parameters: [
{
name: "plugin",
in: "path",
required: true,
schema: { type: "string" },
description: "Plugin name"
},
{
name: "step",
in: "path",
required: true,
schema: { type: "string" },
description: "Step name"
}
],
requestBody: {
content: {
"application/json": {
schema: endpoint.inputSchema || { type: "object" }
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean" },
data: endpoint.outputSchema || { type: "object" },
meta: {
type: "object",
properties: {
timestamp: { type: "string", format: "date-time" }
}
}
}
}
}
}
},
400: {
description: "Validation error",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" }
}
}
},
401: {
description: "Authentication required",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" }
}
}
},
500: {
description: "Internal server error",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" }
}
}
}
},
security: endpoint.requiresAuth ? [{ bearerAuth: [] }] : []
};
}
paths["/session"] = {
get: {
summary: "Check session validity",
description: "Verify if the current session is valid",
tags: ["Session Management"],
security: [{ bearerAuth: [] }],
responses: {
200: {
description: "Session check result",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/SessionResponse" }
}
}
}
}
},
post: {
summary: "Create new session",
description: "Create a new session for a subject",
tags: ["Session Management"],
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
subjectType: { type: "string" },
subjectId: { type: "string" },
ttlSeconds: { type: "number" }
},
required: ["subjectType", "subjectId"]
}
}
}
},
responses: {
201: {
description: "Session created successfully",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/SessionResponse" }
}
}
}
}
},
delete: {
summary: "Destroy session",
description: "Destroy the current session",
tags: ["Session Management"],
security: [{ bearerAuth: [] }],
responses: {
200: {
description: "Session destroyed successfully",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/SuccessResponse" }
}
}
}
}
}
};
paths["/plugins"] = {
get: {
summary: "List all plugins",
description: "Get a list of all available authentication plugins",
tags: ["Plugin Introspection"],
responses: {
200: {
description: "List of plugins",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/PluginListResponse" }
}
}
}
}
}
};
paths["/plugins/{plugin}"] = {
get: {
summary: "Get plugin details",
description: "Get detailed information about a specific plugin",
tags: ["Plugin Introspection"],
parameters: [
{
name: "plugin",
in: "path",
required: true,
schema: { type: "string" },
description: "Plugin name"
}
],
responses: {
200: {
description: "Plugin details",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/PluginDetailsResponse" }
}
}
},
404: {
description: "Plugin not found",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" }
}
}
}
}
}
};
paths["/health"] = {
get: {
summary: "Health check",
description: "Get the health status of the authentication service",
tags: ["Health"],
responses: {
200: {
description: "Health status",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/HealthResponse" }
}
}
}
}
}
};
return {
openapi: "3.0.3",
info: {
title: "ReAuth V2 HTTP API",
description: "HTTP API for ReAuth V2 Authentication Engine",
version: "2.0.0",
contact: {
name: "ReAuth",
url: "https://github.com/SOG-web/reauth"
},
license: {
name: "MIT",
url: "https://opensource.org/licenses/MIT"
}
},
paths,
components: {
securitySchemes: {
bearerAuth: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT"
}
},
schemas: {
ErrorResponse: {
type: "object",
properties: {
success: { type: "boolean", example: false },
error: {
type: "object",
properties: {
code: { type: "string" },
message: { type: "string" },
details: { type: "object" }
}
},
meta: {
type: "object",
properties: {
timestamp: { type: "string", format: "date-time" }
}
}
}
},
SuccessResponse: {