/** * Install AI customization elements from collection with source priority support * Supports all element types: personas, skills, templates, agents, memories, ensembles * * FEATURE (Issue #1447): Added source priority support - checks local → GitHub → collection * in priority order during installation to prevent duplicate installations. * * SECURITY FIX (2025-08-12): Fixed critical vulnerability where content was written to disk * BEFORE validation was complete. This could allow malicious content to persist on the * filesystem even when validation failed. The fix implements: * * 1. VALIDATE-BEFORE-WRITE PATTERN: All content validation (ContentValidator.sanitizePersonaContent, * SecureYamlParser.safeMatter, metadata validation, etc.) is now performed BEFORE any disk operations. * * 2. ATOMIC FILE OPERATIONS: Uses temporary file + atomic rename to prevent partial file corruption * and ensure complete cleanup on any failure during the write process. * * 3. GUARANTEED CLEANUP: If any part of the write operation fails, temporary files are automatically * cleaned up, preventing orphaned malicious content on the filesystem. * * The vulnerability existed in installContent() where fs.writeFile() was called after validation * but before final success confirmation, creating a window where malicious content could persist. */ import { GitHubClient } from './GitHubClient.js'; import { IElementMetadata } from '../types/elements/IElement.js'; import { PortfolioManager, ElementType } from '../portfolio/PortfolioManager.js'; import { ElementSource } from '../config/sourcePriority.js'; import { UnifiedIndexManager } from '../portfolio/UnifiedIndexManager.js'; import { IFileOperationsService } from '../services/FileOperationsService.js'; /** * Result of an element installation operation */ export interface InstallResult { /** Whether the installation succeeded */ success: boolean; /** User-friendly message describing the result */ message: string; /** Element metadata (if installation succeeded) */ metadata?: IElementMetadata; /** Element type */ elementType?: ElementType; /** Filename of the installed element */ filename?: string; /** Source from which the element was installed */ source?: ElementSource; /** Whether the element already existed and was skipped */ alreadyExists?: boolean; } /** * Options for element installation */ export interface InstallOptions { /** Preferred source to install from (overrides priority order) */ preferredSource?: ElementSource; /** Force overwrite even if element exists locally */ force?: boolean; /** Enable fallback to next source if current source fails */ fallbackOnError?: boolean; } export declare class ElementInstaller { private githubClient; private portfolioManager; private baseUrl; private readonly sourcePriorityConfig; private readonly unifiedIndexManager; private readonly fileOperations; constructor(githubClient: GitHubClient, options: { portfolioManager: PortfolioManager; unifiedIndexManager: UnifiedIndexManager; fileOperations?: IFileOperationsService; }); /** * Install element with source priority support (Issue #1447) * * Checks sources in priority order (local → GitHub → collection by default): * 1. Checks if element already exists locally (prevents duplicate installation) * 2. If not local, attempts GitHub portfolio (if specified) * 3. Falls back to collection (existing behavior) * * @param elementName - Name of the element to install * @param elementType - Type of element (persona, skill, etc.) * @param collectionPath - Path in collection (for collection source) * @param options - Installation options * @returns InstallResult with success status and details * * @example * // Install from best available source (checks local → GitHub → collection) * const result = await installer.installElement('creative-writer', ElementType.PERSONA, * 'library/personas/writing/creative-writer.md'); * * @example * // Force install from collection even if exists locally * const result = await installer.installElement('creative-writer', ElementType.PERSONA, * 'library/personas/writing/creative-writer.md', { force: true }); * * @example * // Install from specific source * const result = await installer.installElement('creative-writer', ElementType.PERSONA, * 'library/personas/writing/creative-writer.md', * { preferredSource: ElementSource.COLLECTION }); * * REFACTORED: Reduced cognitive complexity by extracting helper methods * ENHANCEMENT (PR #1453): Added input validation for elementName * ENHANCEMENT (PR #1453): Added structured logging for debugging */ installElement(elementName: string, elementType: ElementType, collectionPath: string, options?: InstallOptions): Promise; /** * Check local element existence if force is not enabled * Extracted from installElement() to reduce cognitive complexity * * @param elementName - Name of element to check * @param elementType - Type of element * @param force - Whether to skip the check * @returns InstallResult if element exists, null otherwise * @private */ private checkLocalElementIfNeeded; /** * Determine source priority order based on preferred source * Extracted from installElement() to reduce cognitive complexity * * @param preferredSource - Optional preferred source to prioritize * @returns Array of sources in priority order * @private */ private determineSourcePriority; /** * Try installing from sources in priority order * Extracted from installElement() to reduce cognitive complexity * * @param sourcePriority - Sources to try in order * @param elementName - Name of element * @param elementType - Type of element * @param collectionPath - Collection path * @param fallbackOnError - Whether to continue on errors * @returns InstallResult * @throws Error if all sources fail * @private */ private trySourcesInOrder; /** * Try installing from a single source with error handling * Extracted from trySourcesInOrder() to reduce cognitive complexity * * ENHANCEMENT (PR #1453): Added detailed error context and retry suggestions * ENHANCEMENT (PR #1453): Added structured logging for debugging * * @param source - Source to try * @param elementName - Name of element * @param elementType - Type of element * @param collectionPath - Collection path * @param fallbackOnError - Whether to continue on errors * @returns Object with success flag and optional error * @private */ private tryInstallFromSource; /** * Format detailed error message with context and retry suggestions * ENHANCEMENT (PR #1453): Provides helpful guidance to users when installation fails * * @param source - Source that failed * @param elementName - Name of element * @param error - Original error * @returns Formatted error message with suggestions * @private */ private formatInstallationError; /** * Create comprehensive error for when all sources fail * Extracted to reduce cognitive complexity * * ENHANCEMENT (PR #1453): Added retry suggestions when all sources fail * * @param errors - Array of errors from each source * @returns Error with formatted summary and suggestions * @private */ private createAllSourcesFailedError; /** * Install element from a specific source * * @param source - Source to install from * @param elementName - Name of element * @param elementType - Type of element * @param collectionPath - Path in collection (for collection source) * @returns InstallResult * @private */ private installFromSource; /** * Check if element exists locally * * ENHANCEMENT (PR #1453): Added optional chaining for robustness * * @param elementName - Name of element to check * @param elementType - Type of element * @returns true if element exists locally * @private */ private checkLocalElement; /** * Install element from GitHub portfolio (Issue #1447) * * Fetches element from user's GitHub dollhouse-portfolio repository. * * ENHANCEMENT (PR #1453): Added elementName validation before filename generation * ENHANCEMENT (PR #1453): Added optional chaining for robustness * ENHANCEMENT (PR #1453): Added structured logging for debugging * * @param elementName - Name of element to install * @param elementType - Type of element * @returns InstallResult * @private */ private installFromGitHub; /** * Install element from collection (refactored from installContent) * * REFACTORED: Reduced cognitive complexity by extracting validation and processing steps * ENHANCEMENT (PR #1453): Added structured logging for debugging * * @param collectionPath - Path in collection * @returns InstallResult * @private */ private installFromCollection; /** * Validate collection path and extract element type * Extracted from installFromCollection() to reduce cognitive complexity * * @param sanitizedPath - Sanitized collection path * @returns Element type * @throws Error if path format is invalid * @private */ private validateAndExtractElementType; /** * Fetch content from collection with size validation * Extracted from installFromCollection() to reduce cognitive complexity * * @param sanitizedPath - Sanitized collection path * @returns Decoded content string * @throws Error if fetch fails or size exceeds limits * @private */ private fetchCollectionContent; /** * Validate and sanitize collection content * Extracted from installFromCollection() to reduce cognitive complexity * * @param content - Raw content from collection * @returns Sanitized content and parsed metadata * @throws Error if validation fails * @private */ private validateCollectionContent; /** * Remove prototype-pollution keys from metadata objects. */ private sanitizeMetadata; /** * Parse YAML content with security error handling * Extracted to reduce cognitive complexity * * @param content - Content to parse * @returns Parsed matter result with metadata * @throws Error if parsing fails or security threat detected * @private */ private parseSecureYaml; /** * Validate metadata for security threats * Extracted to reduce cognitive complexity * * @param metadata - Metadata to validate * @throws Error if validation fails * @private */ private validateMetadataSecurity; /** * Prepare file path for installation * Extracted from installFromCollection() to reduce cognitive complexity * * @param sanitizedPath - Sanitized collection path * @param elementType - Element type * @returns Filename, local path, and element directory * @private */ private prepareFilePath; /** * Check if file already exists * Extracted from installFromCollection() to reduce cognitive complexity * * @param localPath - Local file path to check * @param filename - Filename for error message * @returns InstallResult if file exists, null otherwise * @private */ private checkFileExists; /** * Install AI customization element from the collection (backward compatible) * * DEPRECATED: Use installElement() for source priority support * * Automatically detects element type from path structure * * SECURITY FIX: Implements validate-before-write pattern with atomic operations * to prevent malicious content persistence on validation failure. */ installContent(inputPath: string): Promise<{ success: boolean; message: string; metadata?: IElementMetadata; elementType?: ElementType; filename?: string; }>; /** * Atomic file write operation with guaranteed cleanup on failure * * SECURITY FIX: This method ensures that file writes are atomic and any * failures during the write process will not leave partial or corrupted * files on the filesystem. Uses temporary file + rename for atomicity. * * @param destination - Final destination path for the file * @param content - Content to write to the file * @throws Error if write operation fails (with guaranteed cleanup) */ private atomicWriteFile; /** * Get ElementType from string */ private getElementTypeFromString; /** * Format installation success message */ formatInstallSuccess(metadata: IElementMetadata, filename: string, elementType: ElementType): string; } //# sourceMappingURL=ElementInstaller.d.ts.map