sfcc-dev-mcp
Version:
MCP server for Salesforce B2C Commerce Cloud development assistance including logs, debugging, and development tools
188 lines • 7.42 kB
JavaScript
/**
* Documentation Scanner
*
* Responsible for scanning the documentation directory structure and
* discovering SFCC class documentation files with security validation.
*
* Single Responsibility: File system operations and documentation discovery
*/
import fs from 'fs/promises';
import path from 'path';
import { Logger } from '../../utils/logger.js';
export class DocumentationScanner {
logger;
constructor() {
this.logger = Logger.getChildLogger('DocumentationScanner');
}
/**
* Check if a directory name represents an SFCC-specific directory
* SFCC directories include dw_ prefixed namespaces and TopLevel
* Excludes best-practices and sfra directories
*/
isSFCCDirectory(directoryName) {
// Include dw_ prefixed directories (SFCC namespaces)
if (directoryName.startsWith('dw_')) {
return true;
}
// Include TopLevel directory (contains core JavaScript classes)
if (directoryName === 'TopLevel') {
return true;
}
// Exclude best-practices directory (handled by best practices tools)
if (directoryName === 'best-practices') {
return false;
}
// Exclude sfra directory (handled by SFRA tools)
if (directoryName === 'sfra') {
return false;
}
// Exclude any other non-SFCC directories
return false;
}
/**
* Validate file name for security concerns
*/
validateFileName(fileName) {
// Enhanced security validation - validate file name before path operations
if (!fileName || typeof fileName !== 'string') {
this.logger.warn(`Warning: Invalid file name type: ${fileName}`);
return false;
}
// Prevent null bytes and dangerous characters in the file name itself
if (fileName.includes('\0') || fileName.includes('\x00')) {
this.logger.warn(`Warning: File name contains null bytes: ${fileName}`);
return false;
}
// Prevent path traversal sequences in the file name
if (fileName.includes('..') || fileName.includes('/') || fileName.includes('\\')) {
this.logger.warn(`Warning: File name contains path traversal sequences: ${fileName}`);
return false;
}
// Only allow alphanumeric characters, underscores, hyphens, and dots for file names
if (!/^[a-zA-Z0-9_.-]+$/.test(fileName)) {
this.logger.warn(`Warning: File name contains invalid characters: ${fileName}`);
return false;
}
return true;
}
/**
* Validate file path for security concerns
*/
validateFilePath(filePath, packagePath, docsPath) {
// Additional security validation - ensure the resolved path is within the package directory
const resolvedPath = path.resolve(filePath);
const resolvedPackagePath = path.resolve(packagePath);
const resolvedDocsPath = path.resolve(docsPath);
// Ensure the file is within the package directory and docs directory
if (!resolvedPath.startsWith(resolvedPackagePath) || !resolvedPath.startsWith(resolvedDocsPath)) {
this.logger.warn(`Warning: File path outside allowed directory: ${filePath}`);
return false;
}
// Ensure the file still ends with .md after path resolution
if (!resolvedPath.toLowerCase().endsWith('.md')) {
this.logger.warn(`Warning: File does not reference a markdown file: ${filePath}`);
return false;
}
return true;
}
/**
* Validate file content for security concerns
*/
validateFileContent(content, fileName) {
// Basic content validation
if (!content.trim()) {
this.logger.warn(`Warning: Empty documentation file: ${fileName}`);
return false;
}
// Check for binary content
if (content.includes('\0')) {
this.logger.warn(`Warning: Binary content detected in: ${fileName}`);
return false;
}
return true;
}
/**
* Read and validate a single documentation file
*/
async readDocumentationFile(fileName, packagePath, packageName, docsPath) {
if (!this.validateFileName(fileName)) {
return null;
}
const className = fileName.replace('.md', '');
const filePath = path.join(packagePath, fileName);
if (!this.validateFilePath(filePath, packagePath, docsPath)) {
return null;
}
try {
const resolvedPath = path.resolve(filePath);
const content = await fs.readFile(resolvedPath, 'utf-8');
if (!this.validateFileContent(content, fileName)) {
return null;
}
return {
className,
packageName,
filePath,
content,
};
}
catch (fileError) {
this.logger.warn(`Warning: Could not read file ${fileName}: ${fileError}`);
return null;
}
}
/**
* Scan a single package directory for documentation files
*/
async scanPackageDirectory(packageName, packagePath, docsPath) {
const classInfos = [];
try {
const files = await fs.readdir(packagePath);
for (const file of files) {
// Validate file name type and basic content before processing
if (!file || typeof file !== 'string') {
this.logger.warn(`Warning: Invalid file name type: ${file}`);
continue;
}
if (file.endsWith('.md')) {
const classInfo = await this.readDocumentationFile(file, packagePath, packageName, docsPath);
if (classInfo) {
classInfos.push(classInfo);
}
}
}
}
catch (error) {
this.logger.warn(`Warning: Could not read package ${packageName}: ${error}`);
}
return classInfos;
}
/**
* Scan the docs directory and index all SFCC classes
* Only scans SFCC-specific directories, excluding best-practices and sfra
*/
async scanDocumentation(docsPath) {
const classCache = new Map();
const packages = await fs.readdir(docsPath, { withFileTypes: true });
for (const packageDir of packages) {
if (!packageDir.isDirectory()) {
continue;
}
const packageName = packageDir.name;
// Only scan SFCC-specific directories (dw_ prefixed and TopLevel)
// Exclude best-practices and sfra directories which are handled by other tools
if (!this.isSFCCDirectory(packageName)) {
continue;
}
const packagePath = path.join(docsPath, packageName);
const classInfos = await this.scanPackageDirectory(packageName, packagePath, docsPath);
// Add to cache with normalized keys
for (const classInfo of classInfos) {
const cacheKey = `${packageName}.${classInfo.className}`;
classCache.set(cacheKey, classInfo);
}
}
return classCache;
}
}
//# sourceMappingURL=documentation-scanner.js.map