@kaifronsdal/transcript-viewer
Version:
A web-based viewer for AI conversation transcripts with rollback support
106 lines (87 loc) • 2.68 kB
text/typescript
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);
}