UNPKG

@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
'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: {