@superdangerous/app-framework 4.9.0

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.
Files changed (239) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +652 -0
  3. package/dist/api/logsRouter.d.ts +20 -0
  4. package/dist/api/logsRouter.d.ts.map +1 -0
  5. package/dist/api/logsRouter.js +515 -0
  6. package/dist/api/logsRouter.js.map +1 -0
  7. package/dist/cli/dev-server.d.ts +7 -0
  8. package/dist/cli/dev-server.d.ts.map +1 -0
  9. package/dist/cli/dev-server.js +640 -0
  10. package/dist/cli/dev-server.js.map +1 -0
  11. package/dist/cli/index.d.ts +7 -0
  12. package/dist/cli/index.d.ts.map +1 -0
  13. package/dist/cli/index.js +26 -0
  14. package/dist/cli/index.js.map +1 -0
  15. package/dist/core/StandardServer.d.ts +129 -0
  16. package/dist/core/StandardServer.d.ts.map +1 -0
  17. package/dist/core/StandardServer.js +453 -0
  18. package/dist/core/StandardServer.js.map +1 -0
  19. package/dist/core/apiResponse.d.ts +69 -0
  20. package/dist/core/apiResponse.d.ts.map +1 -0
  21. package/dist/core/apiResponse.js +127 -0
  22. package/dist/core/apiResponse.js.map +1 -0
  23. package/dist/core/healthCheck.d.ts +160 -0
  24. package/dist/core/healthCheck.d.ts.map +1 -0
  25. package/dist/core/healthCheck.js +398 -0
  26. package/dist/core/healthCheck.js.map +1 -0
  27. package/dist/core/index.d.ts +40 -0
  28. package/dist/core/index.d.ts.map +1 -0
  29. package/dist/core/index.js +40 -0
  30. package/dist/core/index.js.map +1 -0
  31. package/dist/core/logger.d.ts +117 -0
  32. package/dist/core/logger.d.ts.map +1 -0
  33. package/dist/core/logger.js +826 -0
  34. package/dist/core/logger.js.map +1 -0
  35. package/dist/core/portUtils.d.ts +71 -0
  36. package/dist/core/portUtils.d.ts.map +1 -0
  37. package/dist/core/portUtils.js +240 -0
  38. package/dist/core/portUtils.js.map +1 -0
  39. package/dist/core/storageService.d.ts +119 -0
  40. package/dist/core/storageService.d.ts.map +1 -0
  41. package/dist/core/storageService.js +405 -0
  42. package/dist/core/storageService.js.map +1 -0
  43. package/dist/desktop/bundler.d.ts +40 -0
  44. package/dist/desktop/bundler.d.ts.map +1 -0
  45. package/dist/desktop/bundler.js +176 -0
  46. package/dist/desktop/bundler.js.map +1 -0
  47. package/dist/desktop/index.d.ts +25 -0
  48. package/dist/desktop/index.d.ts.map +1 -0
  49. package/dist/desktop/index.js +15 -0
  50. package/dist/desktop/index.js.map +1 -0
  51. package/dist/desktop/native-modules.d.ts +66 -0
  52. package/dist/desktop/native-modules.d.ts.map +1 -0
  53. package/dist/desktop/native-modules.js +200 -0
  54. package/dist/desktop/native-modules.js.map +1 -0
  55. package/dist/index.d.ts +29 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +39 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/logging/LogCategories.d.ts +87 -0
  60. package/dist/logging/LogCategories.d.ts.map +1 -0
  61. package/dist/logging/LogCategories.js +205 -0
  62. package/dist/logging/LogCategories.js.map +1 -0
  63. package/dist/middleware/aiErrorHandler.d.ts +31 -0
  64. package/dist/middleware/aiErrorHandler.d.ts.map +1 -0
  65. package/dist/middleware/aiErrorHandler.js +181 -0
  66. package/dist/middleware/aiErrorHandler.js.map +1 -0
  67. package/dist/middleware/auth.d.ts +101 -0
  68. package/dist/middleware/auth.d.ts.map +1 -0
  69. package/dist/middleware/auth.js +230 -0
  70. package/dist/middleware/auth.js.map +1 -0
  71. package/dist/middleware/cors.d.ts +56 -0
  72. package/dist/middleware/cors.d.ts.map +1 -0
  73. package/dist/middleware/cors.js +123 -0
  74. package/dist/middleware/cors.js.map +1 -0
  75. package/dist/middleware/errorHandler.d.ts +13 -0
  76. package/dist/middleware/errorHandler.d.ts.map +1 -0
  77. package/dist/middleware/errorHandler.js +85 -0
  78. package/dist/middleware/errorHandler.js.map +1 -0
  79. package/dist/middleware/fileUpload.d.ts +62 -0
  80. package/dist/middleware/fileUpload.d.ts.map +1 -0
  81. package/dist/middleware/fileUpload.js +175 -0
  82. package/dist/middleware/fileUpload.js.map +1 -0
  83. package/dist/middleware/health.d.ts +48 -0
  84. package/dist/middleware/health.d.ts.map +1 -0
  85. package/dist/middleware/health.js +143 -0
  86. package/dist/middleware/health.js.map +1 -0
  87. package/dist/middleware/index.d.ts +20 -0
  88. package/dist/middleware/index.d.ts.map +1 -0
  89. package/dist/middleware/index.js +18 -0
  90. package/dist/middleware/index.js.map +1 -0
  91. package/dist/middleware/openapi.d.ts +64 -0
  92. package/dist/middleware/openapi.d.ts.map +1 -0
  93. package/dist/middleware/openapi.js +258 -0
  94. package/dist/middleware/openapi.js.map +1 -0
  95. package/dist/middleware/requestLogging.d.ts +22 -0
  96. package/dist/middleware/requestLogging.d.ts.map +1 -0
  97. package/dist/middleware/requestLogging.js +61 -0
  98. package/dist/middleware/requestLogging.js.map +1 -0
  99. package/dist/middleware/session.d.ts +84 -0
  100. package/dist/middleware/session.d.ts.map +1 -0
  101. package/dist/middleware/session.js +189 -0
  102. package/dist/middleware/session.js.map +1 -0
  103. package/dist/middleware/validation.d.ts +1337 -0
  104. package/dist/middleware/validation.d.ts.map +1 -0
  105. package/dist/middleware/validation.js +483 -0
  106. package/dist/middleware/validation.js.map +1 -0
  107. package/dist/services/aiService.d.ts +180 -0
  108. package/dist/services/aiService.d.ts.map +1 -0
  109. package/dist/services/aiService.js +547 -0
  110. package/dist/services/aiService.js.map +1 -0
  111. package/dist/services/conversationStorage.d.ts +38 -0
  112. package/dist/services/conversationStorage.d.ts.map +1 -0
  113. package/dist/services/conversationStorage.js +158 -0
  114. package/dist/services/conversationStorage.js.map +1 -0
  115. package/dist/services/crossPlatformBuffer.d.ts +84 -0
  116. package/dist/services/crossPlatformBuffer.d.ts.map +1 -0
  117. package/dist/services/crossPlatformBuffer.js +246 -0
  118. package/dist/services/crossPlatformBuffer.js.map +1 -0
  119. package/dist/services/index.d.ts +17 -0
  120. package/dist/services/index.d.ts.map +1 -0
  121. package/dist/services/index.js +18 -0
  122. package/dist/services/index.js.map +1 -0
  123. package/dist/services/networkService.d.ts +81 -0
  124. package/dist/services/networkService.d.ts.map +1 -0
  125. package/dist/services/networkService.js +268 -0
  126. package/dist/services/networkService.js.map +1 -0
  127. package/dist/services/queueService.d.ts +112 -0
  128. package/dist/services/queueService.d.ts.map +1 -0
  129. package/dist/services/queueService.js +338 -0
  130. package/dist/services/queueService.js.map +1 -0
  131. package/dist/services/settingsService.d.ts +135 -0
  132. package/dist/services/settingsService.d.ts.map +1 -0
  133. package/dist/services/settingsService.js +425 -0
  134. package/dist/services/settingsService.js.map +1 -0
  135. package/dist/services/systemMonitor.d.ts +208 -0
  136. package/dist/services/systemMonitor.d.ts.map +1 -0
  137. package/dist/services/systemMonitor.js +693 -0
  138. package/dist/services/systemMonitor.js.map +1 -0
  139. package/dist/services/updateService.d.ts +78 -0
  140. package/dist/services/updateService.d.ts.map +1 -0
  141. package/dist/services/updateService.js +252 -0
  142. package/dist/services/updateService.js.map +1 -0
  143. package/dist/services/websocketEvents.d.ts +372 -0
  144. package/dist/services/websocketEvents.d.ts.map +1 -0
  145. package/dist/services/websocketEvents.js +338 -0
  146. package/dist/services/websocketEvents.js.map +1 -0
  147. package/dist/services/websocketServer.d.ts +80 -0
  148. package/dist/services/websocketServer.d.ts.map +1 -0
  149. package/dist/services/websocketServer.js +299 -0
  150. package/dist/services/websocketServer.js.map +1 -0
  151. package/dist/settings/SettingsSchema.d.ts +151 -0
  152. package/dist/settings/SettingsSchema.d.ts.map +1 -0
  153. package/dist/settings/SettingsSchema.js +424 -0
  154. package/dist/settings/SettingsSchema.js.map +1 -0
  155. package/dist/testing/TestServer.d.ts +69 -0
  156. package/dist/testing/TestServer.d.ts.map +1 -0
  157. package/dist/testing/TestServer.js +250 -0
  158. package/dist/testing/TestServer.js.map +1 -0
  159. package/dist/types/index.d.ts +137 -0
  160. package/dist/types/index.d.ts.map +1 -0
  161. package/dist/types/index.js +5 -0
  162. package/dist/types/index.js.map +1 -0
  163. package/dist/utils/appPaths.d.ts +74 -0
  164. package/dist/utils/appPaths.d.ts.map +1 -0
  165. package/dist/utils/appPaths.js +162 -0
  166. package/dist/utils/appPaths.js.map +1 -0
  167. package/dist/utils/fs-utils.d.ts +50 -0
  168. package/dist/utils/fs-utils.d.ts.map +1 -0
  169. package/dist/utils/fs-utils.js +114 -0
  170. package/dist/utils/fs-utils.js.map +1 -0
  171. package/dist/utils/index.d.ts +12 -0
  172. package/dist/utils/index.d.ts.map +1 -0
  173. package/dist/utils/index.js +10 -0
  174. package/dist/utils/index.js.map +1 -0
  175. package/dist/utils/standardConfig.d.ts +61 -0
  176. package/dist/utils/standardConfig.d.ts.map +1 -0
  177. package/dist/utils/standardConfig.js +109 -0
  178. package/dist/utils/standardConfig.js.map +1 -0
  179. package/dist/utils/startupBanner.d.ts +34 -0
  180. package/dist/utils/startupBanner.d.ts.map +1 -0
  181. package/dist/utils/startupBanner.js +169 -0
  182. package/dist/utils/startupBanner.js.map +1 -0
  183. package/dist/utils/startupLogger.d.ts +45 -0
  184. package/dist/utils/startupLogger.d.ts.map +1 -0
  185. package/dist/utils/startupLogger.js +200 -0
  186. package/dist/utils/startupLogger.js.map +1 -0
  187. package/package.json +151 -0
  188. package/src/api/logsRouter.ts +600 -0
  189. package/src/cli/dev-server.ts +803 -0
  190. package/src/cli/index.ts +31 -0
  191. package/src/core/StandardServer.ts +587 -0
  192. package/src/core/apiResponse.ts +202 -0
  193. package/src/core/healthCheck.ts +565 -0
  194. package/src/core/index.ts +80 -0
  195. package/src/core/logger.ts +1092 -0
  196. package/src/core/portUtils.ts +319 -0
  197. package/src/core/storageService.ts +595 -0
  198. package/src/desktop/bundler.ts +271 -0
  199. package/src/desktop/index.ts +18 -0
  200. package/src/desktop/native-modules.ts +289 -0
  201. package/src/index.ts +142 -0
  202. package/src/logging/LogCategories.ts +302 -0
  203. package/src/middleware/aiErrorHandler.ts +278 -0
  204. package/src/middleware/auth.ts +329 -0
  205. package/src/middleware/cors.ts +187 -0
  206. package/src/middleware/errorHandler.ts +103 -0
  207. package/src/middleware/fileUpload.ts +252 -0
  208. package/src/middleware/health.ts +206 -0
  209. package/src/middleware/index.ts +71 -0
  210. package/src/middleware/openapi.ts +305 -0
  211. package/src/middleware/requestLogging.ts +92 -0
  212. package/src/middleware/session.ts +238 -0
  213. package/src/middleware/validation.ts +603 -0
  214. package/src/services/aiService.ts +789 -0
  215. package/src/services/conversationStorage.ts +232 -0
  216. package/src/services/crossPlatformBuffer.ts +341 -0
  217. package/src/services/index.ts +47 -0
  218. package/src/services/networkService.ts +351 -0
  219. package/src/services/queueService.ts +446 -0
  220. package/src/services/settingsService.ts +549 -0
  221. package/src/services/systemMonitor.ts +936 -0
  222. package/src/services/updateService.ts +334 -0
  223. package/src/services/websocketEvents.ts +409 -0
  224. package/src/services/websocketServer.ts +394 -0
  225. package/src/settings/SettingsSchema.ts +664 -0
  226. package/src/testing/TestServer.ts +312 -0
  227. package/src/types/index.ts +154 -0
  228. package/src/utils/appPaths.ts +196 -0
  229. package/src/utils/fs-utils.ts +130 -0
  230. package/src/utils/index.ts +15 -0
  231. package/src/utils/standardConfig.ts +178 -0
  232. package/src/utils/startupBanner.ts +287 -0
  233. package/src/utils/startupLogger.ts +268 -0
  234. package/ui/dist/index.d.mts +1221 -0
  235. package/ui/dist/index.d.ts +1221 -0
  236. package/ui/dist/index.js +73 -0
  237. package/ui/dist/index.js.map +1 -0
  238. package/ui/dist/index.mjs +73 -0
  239. package/ui/dist/index.mjs.map +1 -0
@@ -0,0 +1,595 @@
1
+ /**
2
+ * Storage Service
3
+ * Unified service for secure file operations, user uploads, and internal data storage
4
+ * Provides path sanitization, validation, and consistent file handling
5
+ */
6
+
7
+ import path from "path";
8
+ import * as fsUtils from "../utils/fs-utils.js";
9
+ import { createWriteStream } from "fs";
10
+ import { createLogger } from "./index.js";
11
+ import crypto from "crypto";
12
+
13
+ let logger: any; // Will be initialized when needed
14
+
15
+ function ensureLogger() {
16
+ if (!logger) {
17
+ logger = createLogger("StorageService");
18
+ }
19
+ return logger;
20
+ }
21
+
22
+ // Define safe base directories
23
+ const DATA_DIR = path.join(process.cwd(), "data");
24
+ const BASE_DIRS = {
25
+ attachments: path.join(DATA_DIR, "uploads", "attachments"),
26
+ data: DATA_DIR,
27
+ templates: path.join(DATA_DIR, "templates"),
28
+ uploads: path.join(DATA_DIR, "uploads"),
29
+ logs: path.join(DATA_DIR, "logs"),
30
+ config: path.join(DATA_DIR, "config"),
31
+ } as const;
32
+
33
+ export type BaseDirectory = keyof typeof BASE_DIRS;
34
+
35
+ export interface FileInfo {
36
+ name: string;
37
+ size: number;
38
+ path: string;
39
+ mimeType?: string;
40
+ hash?: string;
41
+ created?: Date;
42
+ modified?: Date;
43
+ }
44
+
45
+ export interface SaveOptions {
46
+ overwrite?: boolean;
47
+ createBackup?: boolean;
48
+ validateContent?: boolean;
49
+ }
50
+
51
+ export interface ReadOptions {
52
+ encoding?: BufferEncoding;
53
+ maxSize?: number;
54
+ }
55
+
56
+ export interface FileUploadOptions {
57
+ maxSize?: number; // bytes
58
+ allowedTypes?: string[]; // file extensions
59
+ destination?: string;
60
+ generateUniqueName?: boolean;
61
+ preserveExtension?: boolean;
62
+ }
63
+
64
+ export interface UploadedFile {
65
+ originalName: string;
66
+ filename: string;
67
+ path: string;
68
+ size: number;
69
+ mimetype?: string;
70
+ extension: string;
71
+ hash?: string;
72
+ }
73
+
74
+ export class StorageService {
75
+ private initialized: boolean = false;
76
+ private readonly maxFileSize: number = 100 * 1024 * 1024; // 100MB
77
+
78
+ async initialize(): Promise<void> {
79
+ if (this.initialized) return;
80
+
81
+ try {
82
+ // Ensure all base directories exist
83
+ for (const dir of Object.values(BASE_DIRS)) {
84
+ await fsUtils.ensureDir(dir);
85
+ }
86
+
87
+ // Ensure .gitignore exists in attachments and data directories
88
+ const gitignoreContent = "*\n!.gitignore\n";
89
+ await fsUtils.writeFile(
90
+ path.join(BASE_DIRS.attachments, ".gitignore"),
91
+ gitignoreContent,
92
+ );
93
+ await fsUtils.writeFile(
94
+ path.join(BASE_DIRS.data, ".gitignore"),
95
+ gitignoreContent,
96
+ );
97
+
98
+ this.initialized = true;
99
+ // Storage service initialized
100
+ } catch (_error) {
101
+ ensureLogger().error("Failed to initialize storage service:", _error);
102
+ throw _error;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Sanitize a filename to prevent path traversal attacks
108
+ */
109
+ sanitizeFilename(filename: string): string {
110
+ if (!filename || typeof filename !== "string") {
111
+ throw new Error("Invalid filename provided");
112
+ }
113
+
114
+ // Remove any path components and keep only the basename
115
+ let sanitized = path.basename(filename);
116
+
117
+ // Remove any remaining special characters that could cause issues
118
+ sanitized = sanitized.replace(/[^a-zA-Z0-9._-]/g, "_");
119
+
120
+ // Ensure the filename is not empty after sanitization
121
+ if (!sanitized || sanitized === "." || sanitized === "..") {
122
+ sanitized = `file_${Date.now()}`;
123
+ }
124
+
125
+ // Limit filename length
126
+ if (sanitized.length > 255) {
127
+ const ext = path.extname(sanitized);
128
+ const name = path.basename(sanitized, ext);
129
+ sanitized = name.substring(0, 255 - ext.length) + ext;
130
+ }
131
+
132
+ return sanitized;
133
+ }
134
+
135
+ /**
136
+ * Validate that a path is within the allowed base directory
137
+ */
138
+ isPathSafe(filePath: string, baseDir: string): boolean {
139
+ try {
140
+ const resolvedPath = path.resolve(filePath);
141
+ const resolvedBase = path.resolve(baseDir);
142
+ return resolvedPath.startsWith(resolvedBase);
143
+ } catch (_error) {
144
+ ensureLogger().error("Error validating path safety:", _error);
145
+ return false;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Get the full safe path for a file
151
+ */
152
+ getSafePath(filename: string, category: BaseDirectory = "data"): string {
153
+ const sanitizedName = this.sanitizeFilename(filename);
154
+ const baseDir = BASE_DIRS[category];
155
+ const fullPath = path.join(baseDir, sanitizedName);
156
+
157
+ if (!this.isPathSafe(fullPath, baseDir)) {
158
+ throw new Error("Invalid file path detected");
159
+ }
160
+
161
+ return fullPath;
162
+ }
163
+
164
+ /**
165
+ * Save a file securely
166
+ */
167
+ async saveFile(
168
+ content: string | Buffer,
169
+ filename: string,
170
+ category: BaseDirectory = "data",
171
+ options: SaveOptions = {},
172
+ ): Promise<FileInfo> {
173
+ await this.initialize();
174
+
175
+ const safePath = this.getSafePath(filename, category);
176
+
177
+ // Check if file exists and handle accordingly
178
+ if (await fsUtils.pathExists(safePath)) {
179
+ if (!options.overwrite) {
180
+ throw new Error(`File already exists: ${filename}`);
181
+ }
182
+ if (options.createBackup) {
183
+ const backupPath = `${safePath}.backup.${Date.now()}`;
184
+ await fsUtils.copy(safePath, backupPath);
185
+ ensureLogger().info(`Created backup: ${backupPath}`);
186
+ }
187
+ }
188
+
189
+ // Validate content size
190
+ const size = Buffer.isBuffer(content)
191
+ ? content.length
192
+ : Buffer.byteLength(content);
193
+ if (size > this.maxFileSize) {
194
+ throw new Error(
195
+ `File size exceeds maximum allowed size of ${this.maxFileSize} bytes`,
196
+ );
197
+ }
198
+
199
+ // Save the file
200
+ await fsUtils.writeFile(safePath, content);
201
+
202
+ // Calculate hash
203
+ const hash = crypto
204
+ .createHash("sha256")
205
+ .update(Buffer.isBuffer(content) ? content : Buffer.from(content))
206
+ .digest("hex");
207
+
208
+ const stats = await fsUtils.stat(safePath);
209
+
210
+ ensureLogger().debug(`File saved: ${safePath} (${size} bytes)`);
211
+
212
+ return {
213
+ name: filename,
214
+ size,
215
+ path: safePath,
216
+ hash,
217
+ created: stats.birthtime,
218
+ modified: stats.mtime,
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Read a file securely
224
+ */
225
+ async readFile(
226
+ filename: string,
227
+ category: BaseDirectory = "data",
228
+ options: ReadOptions = {},
229
+ ): Promise<string | Buffer> {
230
+ await this.initialize();
231
+
232
+ const safePath = this.getSafePath(filename, category);
233
+
234
+ // Check if file exists
235
+ if (!(await fsUtils.pathExists(safePath))) {
236
+ throw new Error(`File not found: ${filename}`);
237
+ }
238
+
239
+ // Check file size
240
+ const stats = await fsUtils.stat(safePath);
241
+ const maxSize = options.maxSize || this.maxFileSize;
242
+
243
+ if (stats.size > maxSize) {
244
+ throw new Error(
245
+ `File size (${stats.size}) exceeds maximum allowed size (${maxSize})`,
246
+ );
247
+ }
248
+
249
+ // Read the file
250
+ const content = options.encoding
251
+ ? await fsUtils.readFile(safePath, options.encoding)
252
+ : await fsUtils.readFile(safePath);
253
+
254
+ ensureLogger().debug(`File read: ${safePath} (${stats.size} bytes)`);
255
+
256
+ return content;
257
+ }
258
+
259
+ /**
260
+ * Delete a file securely
261
+ */
262
+ async deleteFile(
263
+ filename: string,
264
+ category: BaseDirectory = "data",
265
+ ): Promise<boolean> {
266
+ await this.initialize();
267
+
268
+ const safePath = this.getSafePath(filename, category);
269
+
270
+ if (!(await fsUtils.pathExists(safePath))) {
271
+ ensureLogger().warn(`File not found for deletion: ${filename}`);
272
+ return false;
273
+ }
274
+
275
+ await fsUtils.unlink(safePath);
276
+ ensureLogger().debug(`File deleted: ${safePath}`);
277
+ return true;
278
+ }
279
+
280
+ /**
281
+ * List files in a category
282
+ */
283
+ async listFiles(
284
+ category: BaseDirectory = "data",
285
+ pattern?: RegExp,
286
+ ): Promise<FileInfo[]> {
287
+ await this.initialize();
288
+
289
+ const baseDir = BASE_DIRS[category];
290
+ const files: FileInfo[] = [];
291
+
292
+ try {
293
+ const entries = await fsUtils.readdir(baseDir);
294
+
295
+ for (const entry of entries) {
296
+ if (pattern && !pattern.test(entry)) continue;
297
+
298
+ const fullPath = path.join(baseDir, entry);
299
+ const stats = await fsUtils.stat(fullPath);
300
+
301
+ if (stats.isFile()) {
302
+ files.push({
303
+ name: entry,
304
+ size: stats.size,
305
+ path: fullPath,
306
+ created: stats.birthtime,
307
+ modified: stats.mtime,
308
+ });
309
+ }
310
+ }
311
+ } catch (_error) {
312
+ ensureLogger().error(`Error listing files in ${category}:`, _error);
313
+ }
314
+
315
+ return files;
316
+ }
317
+
318
+ /**
319
+ * Get file info
320
+ */
321
+ async getFileInfo(
322
+ filename: string,
323
+ category: BaseDirectory = "data",
324
+ ): Promise<FileInfo | null> {
325
+ await this.initialize();
326
+
327
+ const safePath = this.getSafePath(filename, category);
328
+
329
+ if (!(await fsUtils.pathExists(safePath))) {
330
+ return null;
331
+ }
332
+
333
+ const stats = await fsUtils.stat(safePath);
334
+
335
+ return {
336
+ name: filename,
337
+ size: stats.size,
338
+ path: safePath,
339
+ created: stats.birthtime,
340
+ modified: stats.mtime,
341
+ };
342
+ }
343
+
344
+ /**
345
+ * Move file between categories
346
+ */
347
+ async moveFile(
348
+ filename: string,
349
+ fromCategory: BaseDirectory,
350
+ toCategory: BaseDirectory,
351
+ ): Promise<FileInfo> {
352
+ await this.initialize();
353
+
354
+ const sourcePath = this.getSafePath(filename, fromCategory);
355
+ const destPath = this.getSafePath(filename, toCategory);
356
+
357
+ if (!(await fsUtils.pathExists(sourcePath))) {
358
+ throw new Error(`Source file not found: ${filename}`);
359
+ }
360
+
361
+ await fsUtils.move(sourcePath, destPath, { overwrite: false });
362
+
363
+ const stats = await fsUtils.stat(destPath);
364
+
365
+ ensureLogger().debug(
366
+ `File moved from ${fromCategory} to ${toCategory}: ${filename}`,
367
+ );
368
+
369
+ return {
370
+ name: filename,
371
+ size: stats.size,
372
+ path: destPath,
373
+ created: stats.birthtime,
374
+ modified: stats.mtime,
375
+ };
376
+ }
377
+
378
+ /**
379
+ * Get available base directories
380
+ */
381
+ getBaseDirectories(): Record<BaseDirectory, string> {
382
+ return { ...BASE_DIRS };
383
+ }
384
+
385
+ /**
386
+ * Save a user upload with security checks
387
+ * @param file - File buffer or stream
388
+ * @param originalName - Original filename from user
389
+ * @param options - Upload options
390
+ * @returns Information about the uploaded file
391
+ */
392
+ async saveUserUpload(
393
+ file: Buffer | NodeJS.ReadableStream,
394
+ originalName: string,
395
+ options: FileUploadOptions = {},
396
+ ): Promise<UploadedFile> {
397
+ await this.initialize();
398
+
399
+ const {
400
+ maxSize = this.maxFileSize,
401
+ allowedTypes = [],
402
+ destination = "uploads",
403
+ generateUniqueName = true,
404
+ preserveExtension = true,
405
+ } = options;
406
+
407
+ // Sanitize the original filename
408
+ const sanitizedName = this.sanitizeFilename(originalName);
409
+ const ext = path.extname(sanitizedName).toLowerCase();
410
+ const nameWithoutExt = path.basename(sanitizedName, ext);
411
+
412
+ // Check file extension if restrictions are specified
413
+ if (allowedTypes.length > 0 && !allowedTypes.includes(ext)) {
414
+ throw new Error(
415
+ `File type ${ext} is not allowed. Allowed types: ${allowedTypes.join(", ")}`,
416
+ );
417
+ }
418
+
419
+ // Generate filename
420
+ let filename: string;
421
+ if (generateUniqueName) {
422
+ const uniqueId = crypto.randomBytes(8).toString("hex");
423
+ filename = preserveExtension
424
+ ? `${nameWithoutExt}_${uniqueId}${ext}`
425
+ : `${uniqueId}`;
426
+ } else {
427
+ filename = sanitizedName;
428
+ }
429
+
430
+ // Determine the upload path. Prefer known base directories, but allow custom
431
+ // destinations for backward compatibility with the old FileHandler API.
432
+ let uploadPath: string;
433
+ if ((BASE_DIRS as Record<string, string>)[destination]) {
434
+ uploadPath = this.getSafePath(filename, destination as BaseDirectory);
435
+ } else {
436
+ const resolvedDest = path.isAbsolute(destination)
437
+ ? destination
438
+ : path.join(process.cwd(), destination);
439
+ await fsUtils.ensureDir(resolvedDest);
440
+ uploadPath = path.join(resolvedDest, this.sanitizeFilename(filename));
441
+ }
442
+
443
+ // Save the file
444
+ if (Buffer.isBuffer(file)) {
445
+ // Check size for buffer
446
+ if (file.length > maxSize) {
447
+ throw new Error(
448
+ `File size exceeds maximum allowed size of ${maxSize} bytes`,
449
+ );
450
+ }
451
+ await fsUtils.writeFile(uploadPath, file);
452
+ } else {
453
+ // For streams, we need to check size while writing
454
+ const writeStream = createWriteStream(uploadPath);
455
+ let size = 0;
456
+
457
+ await new Promise((resolve, reject) => {
458
+ file.on("data", (chunk: Buffer) => {
459
+ size += chunk.length;
460
+ if (size > maxSize) {
461
+ writeStream.destroy();
462
+ fsUtils.unlink(uploadPath).catch(() => {}); // Clean up partial file
463
+ reject(
464
+ new Error(
465
+ `File size exceeds maximum allowed size of ${maxSize} bytes`,
466
+ ),
467
+ );
468
+ }
469
+ });
470
+
471
+ file.on("error", reject);
472
+ writeStream.on("error", reject);
473
+ writeStream.on("finish", () => resolve(undefined));
474
+
475
+ file.pipe(writeStream);
476
+ });
477
+ }
478
+
479
+ // Generate file hash
480
+ const fileBuffer = await fsUtils.readFile(uploadPath);
481
+ const hash = crypto.createHash("sha256").update(fileBuffer).digest("hex");
482
+
483
+ // Get file stats
484
+ const stats = await fsUtils.stat(uploadPath);
485
+
486
+ const uploadedFile: UploadedFile = {
487
+ originalName,
488
+ filename,
489
+ path: uploadPath,
490
+ size: stats.size,
491
+ extension: ext,
492
+ hash,
493
+ };
494
+
495
+ ensureLogger().info("User file uploaded successfully", {
496
+ originalName,
497
+ filename,
498
+ size: stats.size,
499
+ hash,
500
+ });
501
+
502
+ return uploadedFile;
503
+ }
504
+
505
+ /**
506
+ * Clean temporary files older than a given age. Falls back to ./temp to
507
+ * preserve the previous middleware behaviour.
508
+ */
509
+ async cleanTempFiles(
510
+ maxAgeMs: number = 24 * 60 * 60 * 1000,
511
+ tempDir: string = path.join(process.cwd(), "temp"),
512
+ ): Promise<number> {
513
+ await fsUtils.ensureDir(tempDir);
514
+
515
+ const now = Date.now();
516
+ let deletedCount = 0;
517
+
518
+ try {
519
+ const files = await fsUtils.readdir(tempDir);
520
+
521
+ for (const file of files) {
522
+ const filePath = path.join(tempDir, file);
523
+ const stats = await fsUtils.stat(filePath);
524
+
525
+ if (!stats.isFile()) continue;
526
+
527
+ if (now - stats.mtimeMs > maxAgeMs) {
528
+ await fsUtils.unlink(filePath);
529
+ deletedCount++;
530
+ }
531
+ }
532
+
533
+ if (deletedCount > 0) {
534
+ ensureLogger().info(
535
+ `Cleaned ${deletedCount} temporary file(s) older than ${Math.round(maxAgeMs / (1000 * 60 * 60))}h`,
536
+ );
537
+ }
538
+ } catch (_error: any) {
539
+ ensureLogger().error("Error cleaning temp files:", _error);
540
+ }
541
+
542
+ return deletedCount;
543
+ }
544
+
545
+ /**
546
+ * Save internal application data
547
+ * @param data - Data to save
548
+ * @param filename - Filename for the data
549
+ * @param category - Category/directory for the data
550
+ * @param options - Save options
551
+ */
552
+ async saveInternalData(
553
+ data: string | Buffer | object,
554
+ filename: string,
555
+ category: BaseDirectory = "data",
556
+ options: SaveOptions = {},
557
+ ): Promise<FileInfo> {
558
+ // If data is an object, stringify it
559
+ const finalData =
560
+ typeof data === "object" && !Buffer.isBuffer(data)
561
+ ? JSON.stringify(data, null, 2)
562
+ : data;
563
+
564
+ // Use the existing save method
565
+ return this.saveFile(
566
+ finalData as string | Buffer,
567
+ filename,
568
+ category,
569
+ options,
570
+ );
571
+ }
572
+ }
573
+
574
+ // Singleton instance
575
+ let storageService: StorageService | null = null;
576
+
577
+ export function getStorageService(): StorageService {
578
+ if (!storageService) {
579
+ storageService = new StorageService();
580
+ }
581
+ return storageService;
582
+ }
583
+
584
+ // Backward compatibility export (deprecated)
585
+ export function getSecureFileHandler(): StorageService {
586
+ console.warn(
587
+ "getSecureFileHandler is deprecated. Use getStorageService instead.",
588
+ );
589
+ return getStorageService();
590
+ }
591
+
592
+ // Export for backward compatibility (deprecated)
593
+ export { StorageService as SecureFileHandler };
594
+
595
+ export default StorageService;