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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/build.mjs +40 -0
- package/cli.js +2 -0
- package/package.json +44 -0
- package/public/app.jsx +1335 -0
- package/public/bundle.js +111 -0
- package/public/bundle.js.map +7 -0
- package/public/favicon.ico +0 -0
- package/public/icons/apple-touch-icon.png +0 -0
- package/public/icons/mask-icon.svg +3 -0
- package/public/icons/most.png +0 -0
- package/public/icons/pwa-192x192.png +0 -0
- package/public/icons/pwa-512x512.png +0 -0
- package/public/index.html +15 -0
- package/public/index.jsx +5 -0
- package/server.js +615 -0
- package/src/config.js +22 -0
- package/src/core/cid.js +141 -0
- package/src/index.js +1073 -0
- package/src/utils/errors.js +66 -0
- package/src/utils/security.js +166 -0
|
@@ -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
|
+
|