@raven-js/glean
Version:
Glean documentation gold from your codebase - JSDoc parsing, validation, and beautiful doc generation
133 lines (121 loc) • 3.9 kB
JavaScript
/**
* @author Anonyfox <max@anonyfox.com>
* @license MIT
* @see {@link https://github.com/Anonyfox/ravenjs}
* @see {@link https://ravenjs.dev}
* @see {@link https://anonyfox.com}
*/
/**
* Entity page route handler for /modules/{moduleName}/{entityName}/ route.
*
* Implements dual parameter resolution, comprehensive validation, error handling,
* and SEO-optimized responses for individual API entity documentation pages.
*/
import { extractEntityPageData } from "../data/entity-page.js";
import { entityPageTemplate } from "../templates/entity-page.js";
/**
* Creates entity page handler with security validation and error handling.
*
* Validates module and entity names, extracts documentation data, and renders
* HTML with appropriate security and caching headers.
*
* @param {import('../../extract/models/package.js').Package} packageInstance - The package data instance.
* @returns {Function} Wings route handler function
*
* @example
* // Create entity route handler
* const handler = createEntityPageHandler(packageInstance);
* app.get('/modules/:moduleName/:entityName/', handler);
*/
export function createEntityPageHandler(packageInstance, options = {}) {
const { urlBuilder } = /** @type {any} */ (options);
/**
* Handle entity documentation page requests
* @param {import('@raven-js/wings').Context} ctx - Wings request context
*/
return async (ctx) => {
try {
const moduleName = ctx.pathParams.moduleName;
const entityName = ctx.pathParams.entityName;
// Validate moduleName parameter
if (
!moduleName ||
typeof moduleName !== "string" ||
moduleName.trim() === ""
) {
return await ctx.error(
"Module name is required and must be a valid string",
);
}
// Validate entityName parameter
if (
!entityName ||
typeof entityName !== "string" ||
entityName.trim() === ""
) {
return await ctx.error(
"Entity name is required and must be a valid string",
);
}
// Security validation for moduleName
if (
moduleName.includes("..") ||
moduleName.startsWith("/") ||
moduleName.includes("//") ||
moduleName.includes("\\") ||
moduleName.includes("\0")
) {
return await ctx.error(
"Invalid module name format - cannot contain path traversal characters",
);
}
// Security validation for entityName
if (
entityName.includes("..") ||
entityName.startsWith("/") ||
entityName.includes("//") ||
entityName.includes("\\") ||
entityName.includes("\0") ||
entityName.includes("<") ||
entityName.includes(">")
) {
return await ctx.error(
"Invalid entity name format - cannot contain dangerous characters",
);
}
// Additional length validation to prevent abuse
if (moduleName.length > 200 || entityName.length > 200) {
return await ctx.error(
"Module and entity names must be under 200 characters",
);
}
// Extract data for the specific entity
const data = extractEntityPageData(
packageInstance,
moduleName,
entityName,
{ urlBuilder },
);
// Generate HTML using template
const html = entityPageTemplate(/** @type {any} */ (data), {
urlBuilder,
});
// Send HTML response with security and caching headers
await ctx.html(html);
ctx.responseHeaders.set("Cache-Control", "public, max-age=3600");
ctx.responseHeaders.set("X-Content-Type-Options", "nosniff");
ctx.responseHeaders.set("X-Frame-Options", "SAMEORIGIN");
} catch (error) {
// Handle specific error types with appropriate responses
if (error.message?.includes("not found")) {
return await ctx.notFound(`Entity not found: ${error.message}`);
} else {
// Log error for debugging
console.error("Entity page generation error:", error);
return await ctx.error(
`Failed to generate entity page: ${error.message}`,
);
}
}
};
}