import semver from 'semver'; import { __logger } from '../logger/internal-logger'; import { CBFileSystem } from '../node/file-system'; import { AllowedModule, Manifest } from '../types/common'; /** * ManifestValidator provides validation for Chargebee Apps application manifests * Public implementation for validating user applications */ export class ManifestValidator { private allowedModules: AllowedModule[]; private fileSystem: CBFileSystem; constructor(allowedModulesConfig: AllowedModule[], fileSystem: CBFileSystem) { this.allowedModules = allowedModulesConfig || []; this.fileSystem = fileSystem; } /** * Validates a manifest file from disk * @param {string} path - Path to the manifest.json file * @returns {Object} The validated manifest * @throws {Error} If manifest file is invalid or cannot be read */ validateAndGetManifestFile(path: string): Manifest { if (!this.fileSystem.existsSync(path)) { throw new Error(`manifest.json not found in user code directory`); } try { const manifestContent = this.fileSystem.readFileSync(path, 'utf8'); const manifest = JSON.parse(manifestContent); this.validate(manifest); return manifest; } catch (error) { if (error instanceof SyntaxError) { throw new Error(`Failed to parse manifest file: ${error.message}`); } const err = error as Error; throw new Error(`Failed to read manifest file: ${err.message}`); } } /** * Validates a complete manifest object * @param {Manifest} manifest - The manifest object to validate * @returns {Manifest} The validated manifest * @throws {Error} If manifest is invalid */ protected validate(manifest: Manifest): void { // Basic structure validation this.validateBasicStructure(manifest); // Events validation this.validateEvents(manifest); // Dependencies validation this.validateDependencies(manifest); // Metadata validation this.validateMetadata(manifest); } /** * Validates the basic structure of the manifest * @param {Object} manifest - The manifest object * @throws {Error} If basic structure is invalid */ protected validateBasicStructure(manifest: Manifest): void { if (!manifest || typeof manifest !== 'object') { throw new Error('Manifest must be a valid object'); } if (manifest.events && typeof manifest.events !== 'object') { throw new Error('Events mapping must be an object'); } if (manifest.dependencies && typeof manifest.dependencies !== 'object') { throw new Error('Dependencies must be an object'); } } /** * Validates the events configuration * @param {Object} manifest - The manifest object * @throws {Error} If events configuration is invalid */ protected validateEvents(manifest: Manifest): void { if (!manifest.events) { throw new Error('Invalid manifest: missing "events" mapping'); } for (const [eventType, handler] of Object.entries(manifest.events)) { if (!handler || typeof handler !== 'object') { throw new Error(`Handler name for event "${eventType}" must be a non-empty string`); } } } /** * Validates the dependencies configuration * @param {Object} manifest - The manifest object * @throws {Error} If dependencies are invalid */ protected validateDependencies(manifest: Manifest): void { if (!manifest.dependencies) { return; // Dependencies are optional } if (typeof manifest.dependencies !== 'object') { throw new Error('Invalid manifest: "dependencies" must be an object'); } for (const [depName, depVersion] of Object.entries(manifest.dependencies)) { if (typeof depName !== 'string' || typeof depVersion !== 'string') { throw new Error(`Invalid dependency in manifest: "${depName}" must have string name and version`); } // Check if module is allowed const allowedModule = this.allowedModules.find(module => module.name === depName); if (!allowedModule) { throw new Error(`Dependency "${depName}" is not allowed in this environment`); } // Validate version range if (!semver.validRange(depVersion)) { throw new Error(`Invalid version range "${depVersion}" for dependency "${depName}"`); } // Check version constraints if specified if (allowedModule.version) { if (!semver.satisfies(depVersion, allowedModule.version)) { __logger.warn(`Dependency "${depName}" version range "${depVersion}" differs from allowed version constraint "${allowedModule.version}" - proceeding with warning`); } } else { throw new Error(`Configuration error: Allowed module "${depName}" does not have a version constraint`); } __logger.debug(`Dependency "${depName}" version "${depVersion}" is allowed`); } __logger.debug(`Validated ${Object.keys(manifest.dependencies).length} dependencies in manifest`); } /** * Validates the metadata fields * @param {Object} manifest - The manifest object */ protected validateMetadata(manifest: Manifest): void { // Check for reasonable limits if (manifest.dependencies && Object.keys(manifest.dependencies).length > 10) { __logger.warn('Large number of dependencies detected - consider minimizing dependencies for better performance'); } } }