@re-auth/http-adapters
Version:
HTTP protocol adapters for ReAuth - framework-agnostic integration with Express, Fastify, and Hono
531 lines (528 loc) • 14.8 kB
JavaScript
'use strict';
// 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 NotFoundError = class extends HttpAdapterError {
constructor(message, details) {
super(message, 404, "NOT_FOUND", details);
this.name = "NotFoundError";
}
};
// 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()
}
});
}
}
};
exports.ReAuthHttpAdapter = ReAuthHttpAdapter;
//# sourceMappingURL=base-adapter.cjs.map
//# sourceMappingURL=base-adapter.cjs.map