UNPKG

@kaifronsdal/transcript-viewer

Version:

A web-based viewer for AI conversation transcripts with rollback support

106 lines (87 loc) 2.68 kB
import path from 'path'; /** * Server-side file path utilities with full validation */ /** * Validate that a file path is safe and appropriate for transcript loading */ export function validateFilePath(filePath: string): boolean { // Check for empty or null paths if (!filePath || typeof filePath !== 'string') { return false; } // Normalize the path to check for directory traversal const normalized = path.normalize(filePath); // Reject paths that try to go up directories if (normalized.includes('../') || normalized.startsWith('/')) { return false; } // Reject paths with null bytes (security) if (normalized.includes('\0')) { return false; } // Must end with .json if (!normalized.endsWith('.json')) { return false; } // Must start with transcript_ filename const filename = path.basename(normalized); if (!filename.startsWith('transcript_')) { return false; } return true; } /** * Parse and validate URL path segments server-side */ export function parseAndValidateUrlPath(pathSegments: string[]): string { if (!pathSegments || pathSegments.length === 0) { throw new Error('Invalid path: empty path segments'); } // Decode each segment const decodedSegments = pathSegments.map(segment => { try { return decodeURIComponent(segment); } catch (error) { throw new Error(`Invalid URL encoding in path segment: ${segment}`); } }); // Join segments into a file path const filePath = decodedSegments.join('/'); // Validate the resulting path if (!validateFilePath(filePath)) { throw new Error(`Invalid or unsafe file path: ${filePath}`); } return filePath; } /** * Normalize a file path to consistent format */ export function normalizeFilePath(filePath: string): string { if (!filePath) { throw new Error('Invalid file path: empty string'); } // Remove leading slash if present const cleanPath = filePath.startsWith('/') ? filePath.slice(1) : filePath; // Normalize path separators for cross-platform compatibility return path.normalize(cleanPath).replace(/\\\\/g, '/'); } /** * Extract directory name from file path */ export function extractDirectoryName(filePath: string): string { const dirname = path.dirname(filePath); return dirname === '.' ? 'root' : dirname; } /** * Extract filename without extension */ export function extractFilenameWithoutExtension(filePath: string): string { return path.basename(filePath, '.json'); } /** * Check if a path represents a transcript file */ export function isTranscriptFile(filePath: string): boolean { return validateFilePath(filePath); }