@necto-ai/pgit
Version:
Private file tracking with dual git repositories
351 lines • 13.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ValidationSchemas = exports.InputValidator = void 0;
const path = __importStar(require("path"));
const zod_1 = require("zod");
const specific_errors_1 = require("../errors/specific.errors");
/**
* Input validation service for security and data integrity
*/
class InputValidator {
/**
* Validate file path for security and correctness
*/
static validatePath(inputPath, options = {}) {
const result = {
isValid: true,
normalizedPath: '',
issues: [],
securityRisk: false,
};
// Basic validation
if (!inputPath || typeof inputPath !== 'string') {
result.isValid = false;
result.issues.push('Path must be a non-empty string');
return result;
}
// Trim whitespace but preserve if it's significant
const trimmedPath = inputPath.trim();
if (trimmedPath !== inputPath) {
result.issues.push('Path has leading or trailing whitespace');
result.securityRisk = true;
}
// Check length
const maxLength = options.maxPathLength || this.MAX_PATH_LENGTH;
if (trimmedPath.length > maxLength) {
result.isValid = false;
result.issues.push(`Path exceeds maximum length of ${maxLength} characters`);
return result;
}
// Security pattern checks
for (const pattern of this.DANGEROUS_PATTERNS) {
if (pattern.test(trimmedPath)) {
result.isValid = false;
result.securityRisk = true;
result.issues.push(`Path contains dangerous pattern: ${pattern.source}`);
}
}
// Check for blocked directory names
const pathSegments = trimmedPath.split(/[/\\]/);
for (const segment of pathSegments) {
if (this.BLOCKED_DIRECTORIES.includes(segment.toLowerCase())) {
result.isValid = false;
result.securityRisk = true;
result.issues.push(`Path contains blocked directory: ${segment}`);
}
}
// Absolute path check
if (path.isAbsolute(trimmedPath) && !options.allowAbsolutePaths) {
result.isValid = false;
result.securityRisk = true;
result.issues.push('Absolute paths are not allowed');
}
// Parent directory traversal check
const normalized = path.normalize(trimmedPath);
if (normalized.startsWith('..') && !options.allowParentDirectory) {
result.isValid = false;
result.securityRisk = true;
result.issues.push('Parent directory traversal is not allowed');
}
// Extension validation
if (options.requiredExtensions && options.requiredExtensions.length > 0) {
const ext = path.extname(normalized).toLowerCase();
if (!options.requiredExtensions.includes(ext)) {
result.isValid = false;
result.issues.push(`File must have one of these extensions: ${options.requiredExtensions.join(', ')}`);
}
}
// Custom pattern checks
if (options.blockedPatterns) {
for (const pattern of options.blockedPatterns) {
if (pattern.test(normalized)) {
result.isValid = false;
result.issues.push(`Path matches blocked pattern: ${pattern.source}`);
}
}
}
result.normalizedPath = normalized;
return result;
}
/**
* Validate commit message
*/
static validateCommitMessage(message) {
if (!message || typeof message !== 'string') {
throw new specific_errors_1.MissingArgumentError('Commit message is required', 'commit');
}
const trimmed = message.trim();
if (trimmed.length < 3) {
throw new specific_errors_1.InvalidArgumentError('Commit message must be at least 3 characters', 'commit');
}
if (trimmed.length > 500) {
throw new specific_errors_1.InvalidArgumentError('Commit message must not exceed 500 characters', 'commit');
}
// Check for dangerous characters
// eslint-disable-next-line no-control-regex
if (/[\x00-\x08\x0E-\x1F\x7F]/.test(trimmed)) {
throw new specific_errors_1.SecurityError('Commit message contains invalid control characters', 'commit');
}
}
/**
* Validate branch name
*/
static validateBranchName(branchName) {
if (!branchName || typeof branchName !== 'string') {
throw new specific_errors_1.MissingArgumentError('Branch name is required', 'branch');
}
const trimmed = branchName.trim();
// Git branch name rules
const validBranchName = /^[a-zA-Z0-9._/-]+$/;
if (!validBranchName.test(trimmed)) {
throw new specific_errors_1.InvalidArgumentError('Branch name contains invalid characters', 'branch');
}
// Additional Git restrictions
if (trimmed.startsWith('-') || trimmed.endsWith('.') || trimmed.includes('..')) {
throw new specific_errors_1.InvalidArgumentError('Invalid branch name format', 'branch');
}
if (trimmed.length > 250) {
throw new specific_errors_1.InvalidArgumentError('Branch name is too long (max 250 characters)', 'branch');
}
}
/**
* Validate numeric arguments
*/
static validateNumber(value, field, min, max) {
const num = parseInt(value, 10);
if (isNaN(num)) {
throw new specific_errors_1.InvalidArgumentError(`${field} must be a valid number`, field);
}
if (min !== undefined && num < min) {
throw new specific_errors_1.InvalidArgumentError(`${field} must be at least ${min}`, field);
}
if (max !== undefined && num > max) {
throw new specific_errors_1.InvalidArgumentError(`${field} must be at most ${max}`, field);
}
return num;
}
/**
* Validate command options using Zod schema
*/
static validateOptions(options, schema, command) {
try {
return schema.parse(options);
}
catch (error) {
if (error instanceof zod_1.z.ZodError) {
const issues = error.issues.map(issue => `${issue.path.join('.')}: ${issue.message}`);
throw new specific_errors_1.InvalidInputError(`Invalid options for ${command}: ${issues.join(', ')}`);
}
throw error;
}
}
/**
* Sanitize string input for safe processing
*/
static sanitizeString(input, maxLength = 1000) {
if (typeof input !== 'string') {
throw new specific_errors_1.InvalidInputError('Input must be a string');
}
// Remove control characters except newlines and tabs
// eslint-disable-next-line no-control-regex
let sanitized = input.replace(/[\x00-\x08\x0E-\x1F\x7F]/g, '');
// Trim and limit length
sanitized = sanitized.trim();
if (sanitized.length > maxLength) {
sanitized = sanitized.substring(0, maxLength);
}
return sanitized;
}
/**
* Validate environment and prerequisites
*/
static validateEnvironment() {
// Check Node.js version
const nodeVersion = process.version;
const major = parseInt(nodeVersion.slice(1).split('.')[0], 10);
if (major < 18) {
throw new specific_errors_1.InvalidInputError(`Node.js version ${nodeVersion} is not supported. Minimum required: 18.0.0`);
}
// Check platform support
const supportedPlatforms = ['linux', 'darwin', 'win32'];
if (!supportedPlatforms.includes(process.platform)) {
throw new specific_errors_1.InvalidInputError(`Platform ${process.platform} is not supported`);
}
}
/**
* Validate working directory safety
*/
static validateWorkingDirectory(workingDir) {
const normalized = path.resolve(workingDir);
// Check for system directories
const systemDirs = [
'/bin',
'/sbin',
'/usr/bin',
'/usr/sbin',
'/System',
'/Windows',
'/Program Files',
process.env['SystemRoot'] || '',
].filter(dir => dir.length > 0);
for (const systemDir of systemDirs) {
if (normalized.startsWith(path.resolve(systemDir))) {
throw new specific_errors_1.SecurityError(`Cannot operate in system directory: ${normalized}`);
}
}
// Check for root directory
if (normalized === '/' || /^[A-Z]:\\?$/.test(normalized)) {
throw new specific_errors_1.SecurityError('Cannot operate in root directory');
}
}
/**
* Create safe file path within working directory
*/
static createSafePath(workingDir, relativePath) {
const validation = this.validatePath(relativePath, {
allowAbsolutePaths: false,
allowParentDirectory: false,
});
if (!validation.isValid) {
if (validation.securityRisk) {
throw new specific_errors_1.UnsafePathError(relativePath, validation.issues.join(', '));
}
else {
throw new specific_errors_1.InvalidInputError(`Invalid path: ${validation.issues.join(', ')}`);
}
}
const safePath = path.resolve(workingDir, validation.normalizedPath);
// Ensure the resolved path is still within the working directory
if (!safePath.startsWith(path.resolve(workingDir))) {
throw new specific_errors_1.UnsafePathError(relativePath, 'Path escapes working directory');
}
return safePath;
}
}
exports.InputValidator = InputValidator;
// Security patterns to block
InputValidator.DANGEROUS_PATTERNS = [
/\.\.\//, // Parent directory traversal
/\.\.\\/, // Windows parent directory traversal
/^\//, // Absolute Unix paths (when not allowed)
/^[a-zA-Z]:/, // Windows drive letters (when not allowed)
/\0/, // Null byte injection
/[<>:|"*?]/, // Windows forbidden characters
/^\.+$/, // Only dots (., .., ...)
/\s$/, // Trailing whitespace
/^\s/, // Leading whitespace
];
// Blocked directory names
InputValidator.BLOCKED_DIRECTORIES = [
'.git',
'.private-storage',
'.private-config.json',
'node_modules',
'System Volume Information',
'$Recycle.Bin',
'System32',
];
// Maximum safe path length
InputValidator.MAX_PATH_LENGTH = 255;
/**
* Zod schemas for common validation patterns
*/
exports.ValidationSchemas = {
/**
* Basic command options
*/
basicOptions: zod_1.z.object({
verbose: zod_1.z.boolean().optional(),
}),
/**
* Log command options
*/
logOptions: zod_1.z.object({
verbose: zod_1.z.boolean().optional(),
maxCount: zod_1.z.number().min(1).max(1000).optional(),
oneline: zod_1.z.boolean().optional(),
}),
/**
* Diff command options
*/
diffOptions: zod_1.z.object({
verbose: zod_1.z.boolean().optional(),
cached: zod_1.z.boolean().optional(),
nameOnly: zod_1.z.boolean().optional(),
}),
/**
* Branch command options
*/
branchOptions: zod_1.z.object({
verbose: zod_1.z.boolean().optional(),
create: zod_1.z.boolean().optional(),
}),
/**
* Commit options
*/
commitOptions: zod_1.z.object({
verbose: zod_1.z.boolean().optional(),
message: zod_1.z.string().min(3).max(500).optional(),
}),
/**
* Cleanup options
*/
cleanupOptions: zod_1.z.object({
verbose: zod_1.z.boolean().optional(),
force: zod_1.z.boolean().optional(),
}),
};
//# sourceMappingURL=input.validator.js.map