most-box 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Custom Error Classes
3
+ * Helps categorize errors for better frontend handling
4
+ */
5
+
6
+ export class AppError extends Error {
7
+ constructor(message, code = 'UNKNOWN') {
8
+ super(message)
9
+ this.name = 'AppError'
10
+ this.code = code
11
+ }
12
+ }
13
+
14
+ export class ValidationError extends AppError {
15
+ constructor(message) {
16
+ super(message, 'VALIDATION_ERROR')
17
+ this.name = 'ValidationError'
18
+ }
19
+ }
20
+
21
+ export class FileSizeError extends AppError {
22
+ constructor(message, size) {
23
+ super(message, 'FILE_SIZE_ERROR')
24
+ this.name = 'FileSizeError'
25
+ this.size = size
26
+ }
27
+ }
28
+
29
+ export class PathSecurityError extends AppError {
30
+ constructor(message = 'Path validation failed') {
31
+ super(message, 'PATH_SECURITY_ERROR')
32
+ this.name = 'PathSecurityError'
33
+ }
34
+ }
35
+
36
+ export class PeerNotFoundError extends AppError {
37
+ constructor(message = 'No peers found. Please ensure the publisher is online.') {
38
+ super(message, 'PEER_NOT_FOUND')
39
+ this.name = 'PeerNotFoundError'
40
+ }
41
+ }
42
+
43
+ export class IntegrityError extends AppError {
44
+ constructor(message = 'File integrity check failed. File may be corrupted or tampered.') {
45
+ super(message, 'INTEGRITY_ERROR')
46
+ this.name = 'IntegrityError'
47
+ }
48
+ }
49
+
50
+ export class PermissionError extends AppError {
51
+ constructor(message = 'Permission denied') {
52
+ super(message, 'PERMISSION_ERROR')
53
+ this.name = 'PermissionError'
54
+ }
55
+ }
56
+
57
+ export class EngineNotInitializedError extends AppError {
58
+ constructor(message = 'Engine not initialized. Call start() first.') {
59
+ super(message, 'ENGINE_NOT_INITIALIZED')
60
+ this.name = 'EngineNotInitializedError'
61
+ }
62
+ }
63
+
64
+ export function isErrorWithCode(err, code) {
65
+ return err && err.code === code
66
+ }
@@ -0,0 +1,166 @@
1
+ import path from 'node:path'
2
+ import fs from 'node:fs'
3
+
4
+ import { MAX_FILE_SIZE } from '../config.js'
5
+
6
+ const DANGEROUS_CHARS = /[<>:"|?*\x00-\x1f]/g
7
+ const DANGEROUS_PREFIXES = /^[\s.]+|[\s.]+$/
8
+ const RESERVED_NAMES = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i
9
+
10
+ /**
11
+ * Sanitize filename to prevent security issues
12
+ * @param {string} filename - Original filename
13
+ * @returns {string} - Sanitized filename
14
+ */
15
+ export function sanitizeFilename(filename) {
16
+ if (typeof filename !== 'string') {
17
+ throw new Error('Filename must be a string')
18
+ }
19
+
20
+ let sanitized = filename
21
+
22
+ // Normalize backslashes to forward slashes (S3-style folder paths)
23
+ sanitized = sanitized.replace(/\\/g, '/')
24
+
25
+ // Remove dangerous characters but preserve / for folder paths
26
+ sanitized = sanitized.replace(/[<>:"|?*\x00-\x1f]/g, '_')
27
+
28
+ // Remove dangerous prefixes/suffixes
29
+ sanitized = sanitized.replace(DANGEROUS_PREFIXES, '')
30
+
31
+ // Prevent path traversal
32
+ sanitized = sanitized.replace(/\.\./g, '_')
33
+
34
+ // Normalize multiple consecutive slashes
35
+ sanitized = sanitized.replace(/\/{2,}/g, '/')
36
+
37
+ // Remove leading/trailing slashes
38
+ sanitized = sanitized.replace(/^\/+|\/+$/g, '')
39
+
40
+ // Sanitize each path segment individually
41
+ const segments = sanitized.split('/')
42
+ const safeSegments = segments.map(seg => {
43
+ let safe = seg.replace(/[<>:"|?*]/g, '_')
44
+ const baseName = safe.replace(/\.[^.]+$/, '')
45
+ if (RESERVED_NAMES.test(baseName)) {
46
+ safe = '_' + safe
47
+ }
48
+ return safe.substring(0, 255) || 'unnamed'
49
+ })
50
+
51
+ sanitized = safeSegments.join('/')
52
+
53
+ return sanitized || 'unnamed_file'
54
+ }
55
+
56
+ /**
57
+ * Validate and sanitize file path to prevent path traversal attacks
58
+ * @param {string} inputPath - User input path
59
+ * @param {object} options - Validation options
60
+ * @param {string} [options.allowedBase] - Base directory that paths must be within (optional)
61
+ * @returns {{ cleanPath: string, error?: string }}
62
+ */
63
+ export function validateAndSanitizePath(inputPath, options = {}) {
64
+ if (typeof inputPath !== 'string') {
65
+ return { cleanPath: '', error: 'Path must be a string' }
66
+ }
67
+
68
+ let cleanPath = inputPath
69
+
70
+ cleanPath = cleanPath.replace(/[\u200B-\u200D\uFEFF\u202A-\u202E]/g, '')
71
+
72
+ cleanPath = cleanPath.replace(/"/g, '').trim()
73
+
74
+ const pathTraversalPattern = /\.\./
75
+ if (pathTraversalPattern.test(cleanPath)) {
76
+ return { cleanPath: '', error: 'Path traversal detected: path cannot contain ".."' }
77
+ }
78
+
79
+ cleanPath = path.normalize(cleanPath)
80
+
81
+ if (options.allowedBase) {
82
+ const resolvedPath = path.resolve(cleanPath)
83
+ const allowedBase = path.resolve(options.allowedBase)
84
+ if (!resolvedPath.startsWith(allowedBase)) {
85
+ return { cleanPath: '', error: 'Path must be within allowed directory' }
86
+ }
87
+ }
88
+
89
+ return { cleanPath }
90
+ }
91
+
92
+ /**
93
+ * Check if file size is within limits
94
+ * @param {string} filePath - Path to file
95
+ * @param {number} [maxSize] - Maximum allowed size in bytes (default: 100GB)
96
+ * @returns {{ valid: boolean, size?: number, error?: string }}
97
+ */
98
+ export async function validateFileSize(filePath, maxSize = MAX_FILE_SIZE) {
99
+ try {
100
+ const stats = await fs.promises.stat(filePath)
101
+ const size = stats.size
102
+
103
+ if (!stats.isFile()) {
104
+ return { valid: false, error: 'Path is not a file' }
105
+ }
106
+
107
+ if (size > maxSize) {
108
+ const maxGB = Math.round(maxSize / (1024 * 1024 * 1024))
109
+ return {
110
+ valid: false,
111
+ size,
112
+ error: `File size exceeds limit of ${maxGB} GB`
113
+ }
114
+ }
115
+
116
+ return { valid: true, size }
117
+ } catch (err) {
118
+ if (err.code === 'ENOENT') {
119
+ return { valid: false, error: 'File does not exist' }
120
+ }
121
+ return { valid: false, error: `Failed to check file size: ${err.message}` }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Check if directory is writable
127
+ * @param {string} dirPath - Directory path to check
128
+ * @returns {{ writable: boolean, error?: string }}
129
+ */
130
+ export async function checkDirectoryWritable(dirPath) {
131
+ try {
132
+ if (!fs.existsSync(dirPath)) {
133
+ fs.mkdirSync(dirPath, { recursive: true })
134
+ }
135
+
136
+ const testFile = path.join(dirPath, '.write-test-' + Date.now())
137
+ await fs.promises.writeFile(testFile, 'test')
138
+ await fs.promises.unlink(testFile)
139
+
140
+ return { writable: true }
141
+ } catch (err) {
142
+ return {
143
+ writable: false,
144
+ error: `Cannot write to directory: ${err.message}`
145
+ }
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Get human-readable file size string
151
+ * @param {number} bytes - Size in bytes
152
+ * @returns {string}
153
+ */
154
+ export function formatFileSize(bytes) {
155
+ const units = ['B', 'KB', 'MB', 'GB', 'TB']
156
+ let unitIndex = 0
157
+ let size = bytes
158
+
159
+ while (size >= 1024 && unitIndex < units.length - 1) {
160
+ size /= 1024
161
+ unitIndex++
162
+ }
163
+
164
+ return `${size.toFixed(2)} ${units[unitIndex]}`
165
+ }
166
+