UNPKG

node-msix-packager

Version:

A utility to create MSIX packages from Node.js applications with MCP server support, Node.js Single Executable Application (SEA) bundling using @vercel/ncc and postject, and enhanced build options

189 lines (156 loc) 6.56 kB
const path = require('path'); const fs = require('fs-extra'); const { CONSTANTS, ValidationError } = require('./constants'); /** * Validates configuration object for MSIX package creation * @param {Object} config - Configuration object to validate * @throws {ValidationError} When validation fails */ function validateConfig(config) { const errors = []; // Required fields const requiredFields = [ { field: 'inputPath', type: 'string' }, { field: 'outputPath', type: 'string' }, { field: 'appName', type: 'string' }, { field: 'publisher', type: 'string' } ]; for (const { field, type } of requiredFields) { if (!config[field]) { errors.push(`Missing required field: ${field}`); } else if (typeof config[field] !== type) { errors.push(`Field ${field} must be of type ${type}, got ${typeof config[field]}`); } } // Validate publisher format if (config.publisher && !CONSTANTS.PUBLISHER_PATTERN.test(config.publisher)) { errors.push('Publisher must start with "CN=" (e.g., "CN=MyCompany")'); } // Validate version format if (config.version) { if (!CONSTANTS.VERSION_PATTERN.test(config.version)) { errors.push('Version must be in format x.x.x.x (e.g., "1.0.0.0")'); } } // Validate architecture if (config.architecture && !CONSTANTS.SUPPORTED_ARCHITECTURES.includes(config.architecture)) { errors.push(`Architecture must be one of: ${CONSTANTS.SUPPORTED_ARCHITECTURES.join(', ')}`); } // Validate package name length if (config.packageName && config.packageName.length > CONSTANTS.MAX_PACKAGE_NAME_LENGTH) { errors.push(`Package name must be ${CONSTANTS.MAX_PACKAGE_NAME_LENGTH} characters or less`); } // Validate capabilities if (config.capabilities && !Array.isArray(config.capabilities)) { errors.push('Capabilities must be an array'); } if (errors.length > 0) { throw new ValidationError('config', 'valid configuration object', errors.join('; ')); } } /** * Validates that required paths exist * @param {Object} config - Configuration object * @throws {ValidationError} When paths don't exist */ async function validatePaths(config) { const errors = []; // Check input path exists if (!(await fs.pathExists(config.inputPath))) { errors.push(`Input path does not exist: ${config.inputPath}`); } else { // Check if it's a directory const stats = await fs.stat(config.inputPath); if (!stats.isDirectory()) { errors.push(`Input path must be a directory: ${config.inputPath}`); } // Check for package.json in input directory const packageJsonPath = path.join(config.inputPath, 'package.json'); if (!(await fs.pathExists(packageJsonPath))) { errors.push(`No package.json found in input directory: ${config.inputPath}`); } } // Validate icon path if provided if (config.icon && !(await fs.pathExists(config.icon))) { errors.push(`Icon file does not exist: ${config.icon}`); } if (errors.length > 0) { throw new ValidationError('paths', 'existing paths', errors.join('; ')); } } /** * Sanitizes configuration values * @param {Object} config - Configuration object to sanitize * @returns {Object} Sanitized configuration */ function sanitizeConfig(config) { const sanitized = { ...config }; // Set defaults sanitized.version = sanitized.version || CONSTANTS.DEFAULT_VERSION; sanitized.architecture = sanitized.architecture || CONSTANTS.DEFAULT_ARCHITECTURE; sanitized.executable = sanitized.executable || CONSTANTS.DEFAULT_EXECUTABLE; sanitized.timestampUrl = sanitized.timestampUrl || CONSTANTS.DEFAULT_TIMESTAMP_URL; sanitized.capabilities = sanitized.capabilities || CONSTANTS.DEFAULT_CAPABILITIES; // Sanitize strings sanitized.appName = sanitized.appName.trim(); sanitized.publisher = sanitized.publisher.trim(); // Generate package name if not provided if (!sanitized.packageName) { const publisherName = sanitized.publisher.replace(/^CN=/, '').split(',')[0].trim(); const appNameSafe = sanitized.appName.replace(/[^a-zA-Z0-9]/g, ''); sanitized.packageName = `${publisherName}.${appNameSafe}`.substring(0, CONSTANTS.MAX_PACKAGE_NAME_LENGTH); } // Generate display name if not provided sanitized.displayName = sanitized.displayName || sanitized.appName; // Set description if not provided sanitized.description = sanitized.description || `${sanitized.appName} - Node.js application packaged as MSIX`; // Set build options sanitized.skipBuild = sanitized.skipBuild || false; sanitized.installDevDeps = sanitized.installDevDeps !== false; // Default to true // Normalize paths sanitized.inputPath = path.resolve(sanitized.inputPath); sanitized.outputPath = path.resolve(sanitized.outputPath); if (sanitized.icon) { sanitized.icon = path.resolve(sanitized.icon); } return sanitized; } /** * Validates certificate configuration * @param {Object} signingConfig - Signing configuration * @throws {ValidationError} When signing configuration is invalid */ function validateSigningConfig(signingConfig) { if (!signingConfig.sign) { return; // No validation needed if signing is disabled } const errors = []; // At least one certificate identification method should be provided const hasCertId = signingConfig.certificateThumbprint || signingConfig.certificateSubject || signingConfig.certificatePath; if (!hasCertId) { // This is okay - we'll auto-discover certificates return; } // If certificatePath is provided, check if password is needed if (signingConfig.certificatePath && !signingConfig.certificatePassword) { // Warning but not error - some PFX files don't need passwords } // Validate thumbprint format if provided if (signingConfig.certificateThumbprint) { const thumbprintPattern = /^[A-Fa-f0-9]{40}$/; if (!thumbprintPattern.test(signingConfig.certificateThumbprint.replace(/\s/g, ''))) { errors.push('Certificate thumbprint must be a 40-character hexadecimal string'); } } if (errors.length > 0) { throw new ValidationError('signingConfig', 'valid signing configuration', errors.join('; ')); } } module.exports = { validateConfig, validatePaths, sanitizeConfig, validateSigningConfig };