@flusys/nestjs-storage 7.0.2 → 8.0.0-beta.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.
Files changed (202) hide show
  1. package/config/index.d.ts +3 -3
  2. package/controllers/file-manager.controller.d.ts +4 -4
  3. package/controllers/folder.controller.d.ts +2 -2
  4. package/controllers/index.d.ts +4 -4
  5. package/controllers/storage-config.controller.d.ts +2 -2
  6. package/controllers/upload.controller.d.ts +3 -3
  7. package/docs/index.d.ts +1 -1
  8. package/docs/storage-swagger.config.d.ts +2 -2
  9. package/dtos/index.d.ts +4 -4
  10. package/entities/file-manager-with-company.entity.d.ts +2 -2
  11. package/entities/file-manager.entity.d.ts +1 -1
  12. package/entities/folder-with-company.entity.d.ts +1 -1
  13. package/entities/index.d.ts +15 -15
  14. package/entities/storage-config-with-company.entity.d.ts +1 -1
  15. package/enums/index.d.ts +1 -1
  16. package/fesm/493.js +2768 -0
  17. package/fesm/537.js +1157 -0
  18. package/fesm/562.js +786 -0
  19. package/fesm/{providers/azure-provider.optional.js → chunks/190.js} +33 -30
  20. package/fesm/chunks/252.js +12 -0
  21. package/fesm/chunks/327.js +12 -0
  22. package/fesm/chunks/385.js +12 -0
  23. package/fesm/{providers/s3-provider.optional.js → chunks/591.js} +37 -34
  24. package/fesm/chunks/746.js +12 -0
  25. package/fesm/chunks/798.js +12 -0
  26. package/fesm/{providers/sftp-provider.optional.js → chunks/918.js} +35 -31
  27. package/fesm/config/index.js +119 -3
  28. package/fesm/controllers/index.js +16 -4
  29. package/fesm/docs/index.js +206 -1
  30. package/fesm/dtos/index.js +879 -4
  31. package/fesm/entities/index.js +463 -18
  32. package/fesm/enums/index.js +36 -1
  33. package/fesm/index.js +398 -12
  34. package/fesm/interfaces/index.js +19 -5
  35. package/fesm/modules/index.js +16 -1
  36. package/fesm/providers/index.js +364 -3
  37. package/fesm/runtime.js +252 -0
  38. package/fesm/services/index.js +16 -6
  39. package/fesm/utils/index.js +149 -1
  40. package/index.d.ts +11 -11
  41. package/interfaces/file-manager.interface.d.ts +1 -1
  42. package/interfaces/folder.interface.d.ts +1 -1
  43. package/interfaces/index.d.ts +5 -5
  44. package/interfaces/storage-config.interface.d.ts +1 -1
  45. package/interfaces/storage-module-options.interface.d.ts +3 -3
  46. package/interfaces/storage-provider.interface.d.ts +1 -1
  47. package/middlewares/file-serve.middleware.d.ts +4 -3
  48. package/middlewares/index.d.ts +1 -1
  49. package/modules/index.d.ts +1 -1
  50. package/modules/storage.module.d.ts +2 -2
  51. package/package.json +30 -41
  52. package/providers/azure-provider.optional.d.ts +2 -2
  53. package/providers/index.d.ts +3 -3
  54. package/providers/local-provider.d.ts +2 -2
  55. package/providers/s3-provider.optional.d.ts +2 -2
  56. package/providers/sftp-provider.optional.d.ts +2 -2
  57. package/providers/storage-factory.service.d.ts +3 -3
  58. package/providers/storage-provider.registry.d.ts +2 -2
  59. package/services/file-manager.service.d.ts +9 -8
  60. package/services/folder.service.d.ts +6 -6
  61. package/services/index.d.ts +6 -6
  62. package/services/storage-config.service.d.ts +3 -3
  63. package/services/storage-datasource.provider.d.ts +3 -3
  64. package/services/storage-provider-config.service.d.ts +7 -7
  65. package/services/upload.service.d.ts +6 -6
  66. package/utils/image-compressor.util.d.ts +1 -1
  67. package/utils/index.d.ts +1 -1
  68. package/cjs/config/event-actions.js +0 -34
  69. package/cjs/config/index.js +0 -20
  70. package/cjs/config/message-keys.js +0 -46
  71. package/cjs/config/storage.constants.js +0 -28
  72. package/cjs/controllers/file-manager.controller.js +0 -152
  73. package/cjs/controllers/file-manager.controller.spec.js +0 -135
  74. package/cjs/controllers/folder.controller.js +0 -102
  75. package/cjs/controllers/folder.controller.spec.js +0 -86
  76. package/cjs/controllers/index.js +0 -21
  77. package/cjs/controllers/storage-config.controller.js +0 -102
  78. package/cjs/controllers/storage-config.controller.spec.js +0 -85
  79. package/cjs/controllers/upload.controller.js +0 -234
  80. package/cjs/controllers/upload.controller.spec.js +0 -132
  81. package/cjs/docs/index.js +0 -11
  82. package/cjs/docs/storage-swagger.config.js +0 -188
  83. package/cjs/docs/storage-swagger.config.spec.js +0 -72
  84. package/cjs/dtos/file-manager.dto.js +0 -327
  85. package/cjs/dtos/folder.dto.js +0 -105
  86. package/cjs/dtos/index.js +0 -21
  87. package/cjs/dtos/storage-config.dto.js +0 -170
  88. package/cjs/dtos/upload.dto.js +0 -272
  89. package/cjs/entities/file-manager-with-company.entity.js +0 -61
  90. package/cjs/entities/file-manager.entity.js +0 -122
  91. package/cjs/entities/folder-with-company.entity.js +0 -55
  92. package/cjs/entities/folder.entity.js +0 -64
  93. package/cjs/entities/index.js +0 -63
  94. package/cjs/entities/index.spec.js +0 -18
  95. package/cjs/entities/storage-config-with-company.entity.js +0 -51
  96. package/cjs/entities/storage-config.entity.js +0 -96
  97. package/cjs/enums/file-location.enum.js +0 -17
  98. package/cjs/enums/index.js +0 -18
  99. package/cjs/index.js +0 -29
  100. package/cjs/interfaces/file-manager.interface.js +0 -4
  101. package/cjs/interfaces/folder.interface.js +0 -4
  102. package/cjs/interfaces/index.js +0 -22
  103. package/cjs/interfaces/storage-config.interface.js +0 -4
  104. package/cjs/interfaces/storage-module-options.interface.js +0 -4
  105. package/cjs/interfaces/storage-provider.interface.js +0 -4
  106. package/cjs/middlewares/file-serve.middleware.js +0 -241
  107. package/cjs/middlewares/file-serve.middleware.spec.js +0 -211
  108. package/cjs/middlewares/index.js +0 -18
  109. package/cjs/modules/index.js +0 -18
  110. package/cjs/modules/storage.module.js +0 -165
  111. package/cjs/modules/storage.module.spec.js +0 -184
  112. package/cjs/providers/azure-provider.optional.js +0 -155
  113. package/cjs/providers/azure-provider.optional.spec.js +0 -228
  114. package/cjs/providers/index.js +0 -26
  115. package/cjs/providers/local-provider.js +0 -230
  116. package/cjs/providers/local-provider.spec.js +0 -239
  117. package/cjs/providers/s3-provider.optional.js +0 -175
  118. package/cjs/providers/s3-provider.optional.spec.js +0 -243
  119. package/cjs/providers/sftp-provider.optional.js +0 -203
  120. package/cjs/providers/sftp-provider.optional.spec.js +0 -217
  121. package/cjs/providers/storage-factory.service.js +0 -175
  122. package/cjs/providers/storage-factory.service.spec.js +0 -253
  123. package/cjs/providers/storage-provider.registry.js +0 -41
  124. package/cjs/providers/storage-provider.registry.spec.js +0 -67
  125. package/cjs/services/file-manager.service.js +0 -387
  126. package/cjs/services/file-manager.service.spec.js +0 -580
  127. package/cjs/services/folder.service.js +0 -101
  128. package/cjs/services/folder.service.spec.js +0 -167
  129. package/cjs/services/index.js +0 -23
  130. package/cjs/services/storage-config.service.js +0 -129
  131. package/cjs/services/storage-config.service.spec.js +0 -257
  132. package/cjs/services/storage-datasource.provider.js +0 -137
  133. package/cjs/services/storage-datasource.provider.spec.js +0 -217
  134. package/cjs/services/storage-provider-config.service.js +0 -171
  135. package/cjs/services/storage-provider-config.service.spec.js +0 -265
  136. package/cjs/services/upload.service.js +0 -376
  137. package/cjs/services/upload.service.spec.js +0 -426
  138. package/cjs/utils/file-validator.util.js +0 -440
  139. package/cjs/utils/file-validator.util.spec.js +0 -259
  140. package/cjs/utils/image-compressor.util.js +0 -162
  141. package/cjs/utils/image-compressor.util.spec.js +0 -166
  142. package/cjs/utils/index.js +0 -18
  143. package/fesm/config/event-actions.js +0 -13
  144. package/fesm/config/message-keys.js +0 -22
  145. package/fesm/config/storage.constants.js +0 -7
  146. package/fesm/controllers/file-manager.controller.js +0 -142
  147. package/fesm/controllers/file-manager.controller.spec.js +0 -131
  148. package/fesm/controllers/folder.controller.js +0 -92
  149. package/fesm/controllers/folder.controller.spec.js +0 -82
  150. package/fesm/controllers/storage-config.controller.js +0 -92
  151. package/fesm/controllers/storage-config.controller.spec.js +0 -81
  152. package/fesm/controllers/upload.controller.js +0 -224
  153. package/fesm/controllers/upload.controller.spec.js +0 -128
  154. package/fesm/docs/storage-swagger.config.js +0 -181
  155. package/fesm/docs/storage-swagger.config.spec.js +0 -68
  156. package/fesm/dtos/file-manager.dto.js +0 -300
  157. package/fesm/dtos/folder.dto.js +0 -84
  158. package/fesm/dtos/storage-config.dto.js +0 -149
  159. package/fesm/dtos/upload.dto.js +0 -245
  160. package/fesm/entities/file-manager-with-company.entity.js +0 -51
  161. package/fesm/entities/file-manager.entity.js +0 -112
  162. package/fesm/entities/folder-with-company.entity.js +0 -45
  163. package/fesm/entities/folder.entity.js +0 -54
  164. package/fesm/entities/index.spec.js +0 -14
  165. package/fesm/entities/storage-config-with-company.entity.js +0 -41
  166. package/fesm/entities/storage-config.entity.js +0 -86
  167. package/fesm/enums/file-location.enum.js +0 -7
  168. package/fesm/interfaces/file-manager.interface.js +0 -1
  169. package/fesm/interfaces/folder.interface.js +0 -1
  170. package/fesm/interfaces/storage-config.interface.js +0 -1
  171. package/fesm/interfaces/storage-module-options.interface.js +0 -3
  172. package/fesm/interfaces/storage-provider.interface.js +0 -4
  173. package/fesm/middlewares/file-serve.middleware.js +0 -190
  174. package/fesm/middlewares/file-serve.middleware.spec.js +0 -207
  175. package/fesm/middlewares/index.js +0 -1
  176. package/fesm/modules/storage.module.js +0 -156
  177. package/fesm/modules/storage.module.spec.js +0 -180
  178. package/fesm/providers/azure-provider.optional.spec.js +0 -224
  179. package/fesm/providers/local-provider.js +0 -182
  180. package/fesm/providers/local-provider.spec.js +0 -194
  181. package/fesm/providers/s3-provider.optional.spec.js +0 -239
  182. package/fesm/providers/sftp-provider.optional.spec.js +0 -213
  183. package/fesm/providers/storage-factory.service.js +0 -124
  184. package/fesm/providers/storage-factory.service.spec.js +0 -249
  185. package/fesm/providers/storage-provider.registry.js +0 -34
  186. package/fesm/providers/storage-provider.registry.spec.js +0 -63
  187. package/fesm/services/file-manager.service.js +0 -336
  188. package/fesm/services/file-manager.service.spec.js +0 -576
  189. package/fesm/services/folder.service.js +0 -91
  190. package/fesm/services/folder.service.spec.js +0 -163
  191. package/fesm/services/storage-config.service.js +0 -119
  192. package/fesm/services/storage-config.service.spec.js +0 -253
  193. package/fesm/services/storage-datasource.provider.js +0 -86
  194. package/fesm/services/storage-datasource.provider.spec.js +0 -172
  195. package/fesm/services/storage-provider-config.service.js +0 -161
  196. package/fesm/services/storage-provider-config.service.spec.js +0 -261
  197. package/fesm/services/upload.service.js +0 -366
  198. package/fesm/services/upload.service.spec.js +0 -422
  199. package/fesm/utils/file-validator.util.js +0 -433
  200. package/fesm/utils/file-validator.util.spec.js +0 -255
  201. package/fesm/utils/image-compressor.util.js +0 -114
  202. package/fesm/utils/image-compressor.util.spec.js +0 -162
@@ -1,433 +0,0 @@
1
- /**
2
- * Result of file content validation.
3
- */ /**
4
- * Magic byte signatures for common file types.
5
- * Each entry maps a hex signature pattern to its MIME type.
6
- */ const MAGIC_BYTES = [
7
- // Images
8
- {
9
- signature: [
10
- 0xff,
11
- 0xd8,
12
- 0xff
13
- ],
14
- offset: 0,
15
- mimeType: 'image/jpeg'
16
- },
17
- {
18
- signature: [
19
- 0x89,
20
- 0x50,
21
- 0x4e,
22
- 0x47,
23
- 0x0d,
24
- 0x0a,
25
- 0x1a,
26
- 0x0a
27
- ],
28
- offset: 0,
29
- mimeType: 'image/png'
30
- },
31
- {
32
- signature: [
33
- 0x47,
34
- 0x49,
35
- 0x46,
36
- 0x38,
37
- 0x37,
38
- 0x61
39
- ],
40
- offset: 0,
41
- mimeType: 'image/gif'
42
- },
43
- {
44
- signature: [
45
- 0x47,
46
- 0x49,
47
- 0x46,
48
- 0x38,
49
- 0x39,
50
- 0x61
51
- ],
52
- offset: 0,
53
- mimeType: 'image/gif'
54
- },
55
- {
56
- signature: [
57
- 0x42,
58
- 0x4d
59
- ],
60
- offset: 0,
61
- mimeType: 'image/bmp'
62
- },
63
- {
64
- signature: [
65
- 0x52,
66
- 0x49,
67
- 0x46,
68
- 0x46
69
- ],
70
- offset: 0,
71
- mimeType: 'image/webp'
72
- },
73
- {
74
- signature: [
75
- 0x00,
76
- 0x00,
77
- 0x01,
78
- 0x00
79
- ],
80
- offset: 0,
81
- mimeType: 'image/x-icon'
82
- },
83
- {
84
- signature: [
85
- 0x00,
86
- 0x00,
87
- 0x02,
88
- 0x00
89
- ],
90
- offset: 0,
91
- mimeType: 'image/x-icon'
92
- },
93
- // Documents
94
- {
95
- signature: [
96
- 0x25,
97
- 0x50,
98
- 0x44,
99
- 0x46
100
- ],
101
- offset: 0,
102
- mimeType: 'application/pdf'
103
- },
104
- {
105
- signature: [
106
- 0x50,
107
- 0x4b,
108
- 0x03,
109
- 0x04
110
- ],
111
- offset: 0,
112
- mimeType: 'application/zip'
113
- },
114
- // Audio
115
- {
116
- signature: [
117
- 0x49,
118
- 0x44,
119
- 0x33
120
- ],
121
- offset: 0,
122
- mimeType: 'audio/mpeg'
123
- },
124
- {
125
- signature: [
126
- 0xff,
127
- 0xfb
128
- ],
129
- offset: 0,
130
- mimeType: 'audio/mpeg'
131
- },
132
- {
133
- signature: [
134
- 0xff,
135
- 0xfa
136
- ],
137
- offset: 0,
138
- mimeType: 'audio/mpeg'
139
- },
140
- {
141
- signature: [
142
- 0x4f,
143
- 0x67,
144
- 0x67,
145
- 0x53
146
- ],
147
- offset: 0,
148
- mimeType: 'audio/ogg'
149
- },
150
- {
151
- signature: [
152
- 0x66,
153
- 0x4c,
154
- 0x61,
155
- 0x43
156
- ],
157
- offset: 0,
158
- mimeType: 'audio/flac'
159
- },
160
- // Video
161
- {
162
- signature: [
163
- 0x00,
164
- 0x00,
165
- 0x00,
166
- 0x1c,
167
- 0x66,
168
- 0x74,
169
- 0x79,
170
- 0x70
171
- ],
172
- offset: 0,
173
- mimeType: 'video/mp4'
174
- },
175
- {
176
- signature: [
177
- 0x00,
178
- 0x00,
179
- 0x00,
180
- 0x20,
181
- 0x66,
182
- 0x74,
183
- 0x79,
184
- 0x70
185
- ],
186
- offset: 0,
187
- mimeType: 'video/mp4'
188
- },
189
- {
190
- signature: [
191
- 0x1a,
192
- 0x45,
193
- 0xdf,
194
- 0xa3
195
- ],
196
- offset: 0,
197
- mimeType: 'video/webm'
198
- },
199
- {
200
- signature: [
201
- 0x52,
202
- 0x49,
203
- 0x46,
204
- 0x46
205
- ],
206
- offset: 0,
207
- mimeType: 'video/avi'
208
- },
209
- // Archives
210
- {
211
- signature: [
212
- 0x1f,
213
- 0x8b
214
- ],
215
- offset: 0,
216
- mimeType: 'application/gzip'
217
- },
218
- {
219
- signature: [
220
- 0x37,
221
- 0x7a,
222
- 0xbc,
223
- 0xaf,
224
- 0x27,
225
- 0x1c
226
- ],
227
- offset: 0,
228
- mimeType: 'application/x-7z-compressed'
229
- },
230
- {
231
- signature: [
232
- 0x52,
233
- 0x61,
234
- 0x72,
235
- 0x21,
236
- 0x1a,
237
- 0x07
238
- ],
239
- offset: 0,
240
- mimeType: 'application/x-rar-compressed'
241
- }
242
- ];
243
- /**
244
- * MIME type aliases - types that are equivalent.
245
- */ const MIME_ALIASES = {
246
- 'image/jpeg': [
247
- 'image/jpg'
248
- ],
249
- 'image/jpg': [
250
- 'image/jpeg'
251
- ],
252
- 'video/mp4': [
253
- 'video/quicktime'
254
- ],
255
- 'application/zip': [
256
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
257
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
258
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
259
- ]
260
- };
261
- /**
262
- * File types that are text-based and don't have magic bytes.
263
- * SECURITY NOTE: Dangerous types (HTML, JS, SVG) are excluded as they can contain scripts.
264
- * These types require explicit allowlisting and additional content scanning.
265
- */ const TEXT_BASED_TYPES = [
266
- 'text/plain',
267
- 'text/csv',
268
- 'text/markdown',
269
- 'application/json',
270
- 'application/xml',
271
- 'application/typescript',
272
- 'text/css'
273
- ];
274
- /**
275
- * Dangerous text-based types that can execute scripts.
276
- * These bypass magic-bytes validation but require explicit allowlisting.
277
- */ const DANGEROUS_TEXT_TYPES = [
278
- 'text/html',
279
- 'application/javascript',
280
- 'text/javascript',
281
- 'image/svg+xml',
282
- 'application/xhtml+xml'
283
- ];
284
- /**
285
- * ZIP-based format prefixes that are valid when detected as application/zip.
286
- */ const ZIP_VARIANT_PREFIXES = [
287
- 'application/vnd.openxmlformats-officedocument',
288
- 'application/x-zip',
289
- 'application/x-compressed'
290
- ];
291
- /**
292
- * Utility class for validating file content using magic bytes.
293
- * Prevents file type spoofing by checking actual file content.
294
- */ export class FileValidator {
295
- // Result Helpers
296
- static failureResult(message, detectedType, declaredType) {
297
- return {
298
- valid: false,
299
- detectedType,
300
- declaredType,
301
- message
302
- };
303
- }
304
- static successResult(detectedType, declaredType) {
305
- return {
306
- valid: true,
307
- detectedType,
308
- declaredType
309
- };
310
- }
311
- /**
312
- * Detect file type from buffer using magic bytes.
313
- * @param buffer - File buffer to analyze
314
- * @returns Detected MIME type or null if unknown
315
- */ static detectFileType(buffer) {
316
- for (const { signature, offset, mimeType } of MAGIC_BYTES){
317
- if (buffer.length < offset + signature.length) continue;
318
- let matches = true;
319
- for(let i = 0; i < signature.length; i++){
320
- if (buffer[offset + i] !== signature[i]) {
321
- matches = false;
322
- break;
323
- }
324
- }
325
- if (matches) {
326
- return mimeType;
327
- }
328
- }
329
- return null;
330
- }
331
- /**
332
- * Check if a MIME type is text-based (doesn't have magic bytes).
333
- */ static isTextBasedType(mimeType) {
334
- return TEXT_BASED_TYPES.some((t)=>mimeType.startsWith(t) || mimeType === t);
335
- }
336
- /**
337
- * Check if a MIME type is a dangerous text type that can execute scripts.
338
- */ static isDangerousTextType(mimeType) {
339
- return DANGEROUS_TEXT_TYPES.some((t)=>mimeType === t);
340
- }
341
- /**
342
- * Check if two MIME types are compatible (exact match or aliases).
343
- */ static mimeTypesMatch(detected, declared) {
344
- // Exact match
345
- if (detected === declared) return true;
346
- // Check aliases
347
- const aliases = MIME_ALIASES[detected];
348
- if (aliases?.includes(declared)) return true;
349
- // Check reverse aliases
350
- const reverseAliases = MIME_ALIASES[declared];
351
- if (reverseAliases?.includes(detected)) return true;
352
- // Check if both are in same category (e.g., both images)
353
- const detectedCategory = detected.split('/')[0];
354
- const declaredCategory = declared.split('/')[0];
355
- // For ZIP-based formats, allow any ZIP-detected file if declared is a ZIP variant
356
- if (detected === 'application/zip' && ZIP_VARIANT_PREFIXES.some((v)=>declared.startsWith(v))) {
357
- return true;
358
- }
359
- return detectedCategory === declaredCategory;
360
- }
361
- /**
362
- * Check if a MIME type is in the allowed list.
363
- */ static isTypeAllowed(mimeType, allowedTypes) {
364
- // Wildcard allows all
365
- if (allowedTypes.includes('*/*')) return true;
366
- return allowedTypes.some((allowed)=>{
367
- // Category wildcard (e.g., "image/*")
368
- if (allowed.endsWith('/*')) {
369
- const category = allowed.slice(0, -2);
370
- return mimeType.startsWith(category);
371
- }
372
- return allowed === mimeType;
373
- });
374
- }
375
- /**
376
- * Validate file content matches declared MIME type using magic bytes.
377
- * @param buffer - File buffer
378
- * @param declaredMimeType - MIME type declared by client
379
- * @param allowedTypes - List of allowed MIME types/patterns
380
- * @returns Validation result
381
- */ static validateFileContent(buffer, declaredMimeType, allowedTypes = [
382
- '*/*'
383
- ]) {
384
- try {
385
- const detectedType = this.detectFileType(buffer);
386
- // No magic bytes detected - handle text-based types
387
- if (!detectedType) {
388
- return this.validateUndetectedType(declaredMimeType, allowedTypes);
389
- }
390
- // Verify detected type matches declared type
391
- if (!this.mimeTypesMatch(detectedType, declaredMimeType)) {
392
- return this.failureResult(`File content does not match declared type. Detected: ${detectedType}, Declared: ${declaredMimeType}`, detectedType, declaredMimeType);
393
- }
394
- // Verify type is in allowed list
395
- if (!this.isTypeAllowed(detectedType, allowedTypes)) {
396
- return this.failureResult(`File type "${detectedType}" is not allowed`, detectedType, declaredMimeType);
397
- }
398
- return this.successResult(detectedType, declaredMimeType);
399
- } catch {
400
- return this.failureResult('File validation failed');
401
- }
402
- }
403
- /**
404
- * Handle validation for files without detectable magic bytes.
405
- */ static validateUndetectedType(declaredMimeType, allowedTypes) {
406
- // Check for dangerous text types first (HTML, JS, SVG)
407
- if (this.isDangerousTextType(declaredMimeType)) {
408
- const explicitlyAllowed = allowedTypes.some((t)=>t === declaredMimeType && t !== '*/*' && !t.endsWith('/*'));
409
- if (!explicitlyAllowed) {
410
- return this.failureResult(`File type "${declaredMimeType}" is potentially dangerous and not explicitly allowed`, declaredMimeType, declaredMimeType);
411
- }
412
- }
413
- // Safe text-based files don't have magic bytes, trust declared type
414
- if (this.isTextBasedType(declaredMimeType)) {
415
- const isAllowed = this.isTypeAllowed(declaredMimeType, allowedTypes);
416
- return isAllowed ? this.successResult(declaredMimeType, declaredMimeType) : this.failureResult(`File type "${declaredMimeType}" is not allowed`, declaredMimeType, declaredMimeType);
417
- }
418
- // Binary files without recognized signatures - be cautious
419
- return this.failureResult('Unable to verify file type. File may be corrupted or unsupported.', undefined, declaredMimeType);
420
- }
421
- /**
422
- * Sanitize filename to prevent path traversal and special character issues.
423
- * @param filename - Original filename
424
- * @returns Sanitized filename
425
- */ static sanitizeFilename(filename) {
426
- return filename// Remove path components (prevent traversal)
427
- .replace(/^.*[\\\/]/, '')// Remove null bytes
428
- .replace(/\0/g, '')// Replace multiple dots with single
429
- .replace(/\.{2,}/g, '.')// Remove special characters except allowed ones
430
- .replace(/[^a-zA-Z0-9._-]/g, '_')// Limit length
431
- .substring(0, 255);
432
- }
433
- }
@@ -1,255 +0,0 @@
1
- import { FileValidator } from './file-validator.util';
2
- const JPEG_BYTES = Buffer.from([
3
- 0xff,
4
- 0xd8,
5
- 0xff,
6
- 0xe0,
7
- 0x00,
8
- 0x10
9
- ]);
10
- const PNG_BYTES = Buffer.from([
11
- 0x89,
12
- 0x50,
13
- 0x4e,
14
- 0x47,
15
- 0x0d,
16
- 0x0a,
17
- 0x1a,
18
- 0x0a,
19
- 0x00,
20
- 0x00
21
- ]);
22
- const PDF_BYTES = Buffer.from([
23
- 0x25,
24
- 0x50,
25
- 0x44,
26
- 0x46,
27
- 0x2d,
28
- 0x31,
29
- 0x2e,
30
- 0x34
31
- ]);
32
- const ZIP_BYTES = Buffer.from([
33
- 0x50,
34
- 0x4b,
35
- 0x03,
36
- 0x04,
37
- 0x00,
38
- 0x00
39
- ]);
40
- const GIF87_BYTES = Buffer.from([
41
- 0x47,
42
- 0x49,
43
- 0x46,
44
- 0x38,
45
- 0x37,
46
- 0x61
47
- ]);
48
- const UNKNOWN_BYTES = Buffer.from([
49
- 0x01,
50
- 0x02,
51
- 0x03,
52
- 0x04
53
- ]);
54
- describe('FileValidator', ()=>{
55
- describe('detectFileType', ()=>{
56
- it('detects JPEG from magic bytes', ()=>{
57
- expect(FileValidator.detectFileType(JPEG_BYTES)).toBe('image/jpeg');
58
- });
59
- it('detects PNG from magic bytes', ()=>{
60
- expect(FileValidator.detectFileType(PNG_BYTES)).toBe('image/png');
61
- });
62
- it('detects PDF from magic bytes', ()=>{
63
- expect(FileValidator.detectFileType(PDF_BYTES)).toBe('application/pdf');
64
- });
65
- it('detects ZIP-based formats (docx/xlsx/pptx share this signature)', ()=>{
66
- expect(FileValidator.detectFileType(ZIP_BYTES)).toBe('application/zip');
67
- });
68
- it('detects legacy GIF87a signature', ()=>{
69
- expect(FileValidator.detectFileType(GIF87_BYTES)).toBe('image/gif');
70
- });
71
- it('returns null for unknown/unrecognized signatures', ()=>{
72
- expect(FileValidator.detectFileType(UNKNOWN_BYTES)).toBeNull();
73
- });
74
- it('returns null for a buffer shorter than any known signature', ()=>{
75
- expect(FileValidator.detectFileType(Buffer.from([
76
- 0xff
77
- ]))).toBeNull();
78
- });
79
- it('returns null for an empty buffer', ()=>{
80
- expect(FileValidator.detectFileType(Buffer.alloc(0))).toBeNull();
81
- });
82
- });
83
- describe('isTextBasedType', ()=>{
84
- it('recognizes plain text and structured text types', ()=>{
85
- expect(FileValidator.isTextBasedType('text/plain')).toBe(true);
86
- expect(FileValidator.isTextBasedType('application/json')).toBe(true);
87
- });
88
- it('rejects a non text-based type', ()=>{
89
- expect(FileValidator.isTextBasedType('image/png')).toBe(false);
90
- });
91
- });
92
- describe('isDangerousTextType', ()=>{
93
- it('flags text/html as dangerous', ()=>{
94
- expect(FileValidator.isDangerousTextType('text/html')).toBe(true);
95
- });
96
- it('flags image/svg+xml as dangerous (can embed scripts)', ()=>{
97
- expect(FileValidator.isDangerousTextType('image/svg+xml')).toBe(true);
98
- });
99
- it('does not flag a safe type', ()=>{
100
- expect(FileValidator.isDangerousTextType('image/png')).toBe(false);
101
- });
102
- });
103
- describe('mimeTypesMatch', ()=>{
104
- it('matches identical types', ()=>{
105
- expect(FileValidator.mimeTypesMatch('image/png', 'image/png')).toBe(true);
106
- });
107
- it('matches known aliases (jpeg <-> jpg)', ()=>{
108
- expect(FileValidator.mimeTypesMatch('image/jpeg', 'image/jpg')).toBe(true);
109
- expect(FileValidator.mimeTypesMatch('image/jpg', 'image/jpeg')).toBe(true);
110
- });
111
- it('matches same broad category (image/* vs image/*)', ()=>{
112
- expect(FileValidator.mimeTypesMatch('image/png', 'image/webp')).toBe(true);
113
- });
114
- it('matches ZIP-detected content against an office document declared type', ()=>{
115
- expect(FileValidator.mimeTypesMatch('application/zip', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')).toBe(true);
116
- });
117
- it('does not match unrelated categories', ()=>{
118
- expect(FileValidator.mimeTypesMatch('image/png', 'application/pdf')).toBe(false);
119
- });
120
- });
121
- describe('isTypeAllowed', ()=>{
122
- it('allows anything when wildcard */* is present', ()=>{
123
- expect(FileValidator.isTypeAllowed('application/pdf', [
124
- '*/*'
125
- ])).toBe(true);
126
- });
127
- it('allows category wildcard image/*', ()=>{
128
- expect(FileValidator.isTypeAllowed('image/png', [
129
- 'image/*'
130
- ])).toBe(true);
131
- });
132
- it('rejects a type outside the category wildcard', ()=>{
133
- expect(FileValidator.isTypeAllowed('application/pdf', [
134
- 'image/*'
135
- ])).toBe(false);
136
- });
137
- it('allows exact match', ()=>{
138
- expect(FileValidator.isTypeAllowed('application/pdf', [
139
- 'application/pdf'
140
- ])).toBe(true);
141
- });
142
- it('rejects when not in the explicit allow-list', ()=>{
143
- expect(FileValidator.isTypeAllowed('application/pdf', [
144
- 'image/png',
145
- 'image/jpeg'
146
- ])).toBe(false);
147
- });
148
- });
149
- describe('validateFileContent', ()=>{
150
- it('passes when magic bytes match declared type and type is allowed', ()=>{
151
- const result = FileValidator.validateFileContent(JPEG_BYTES, 'image/jpeg', [
152
- 'image/*'
153
- ]);
154
- expect(result.valid).toBe(true);
155
- expect(result.detectedType).toBe('image/jpeg');
156
- });
157
- it('fails when declared type does not match detected content (MIME spoofing)', ()=>{
158
- const result = FileValidator.validateFileContent(PNG_BYTES, 'application/pdf', [
159
- '*/*'
160
- ]);
161
- expect(result.valid).toBe(false);
162
- expect(result.message).toContain('does not match declared type');
163
- });
164
- it('fails when detected type is not in the allow-list', ()=>{
165
- const result = FileValidator.validateFileContent(PDF_BYTES, 'application/pdf', [
166
- 'image/*'
167
- ]);
168
- expect(result.valid).toBe(false);
169
- expect(result.message).toContain('not allowed');
170
- });
171
- it('trusts declared type for safe text-based content without magic bytes', ()=>{
172
- const result = FileValidator.validateFileContent(Buffer.from('name,age\nJohn,30'), 'text/csv', [
173
- 'text/*'
174
- ]);
175
- expect(result.valid).toBe(true);
176
- expect(result.detectedType).toBe('text/csv');
177
- });
178
- it('rejects dangerous text/html content unless explicitly allow-listed', ()=>{
179
- const result = FileValidator.validateFileContent(Buffer.from('<html><body>hi</body></html>'), 'text/html', [
180
- '*/*'
181
- ]);
182
- expect(result.valid).toBe(false);
183
- expect(result.message).toContain('potentially dangerous');
184
- });
185
- it('rejects image/svg+xml by default (can contain embedded scripts)', ()=>{
186
- const result = FileValidator.validateFileContent(Buffer.from('<svg onload="alert(1)"></svg>'), 'image/svg+xml', [
187
- '*/*'
188
- ]);
189
- expect(result.valid).toBe(false);
190
- });
191
- it('still fails text/html even when explicitly allow-listed, since it is not in TEXT_BASED_TYPES', ()=>{
192
- // NOTE: validateUndetectedType() skips the "dangerous type" rejection when the type is
193
- // explicitly allow-listed, but text/html is absent from TEXT_BASED_TYPES so it falls through
194
- // to the generic "Unable to verify file type" failure. Documenting actual behavior here;
195
- // see final report for this being flagged as a potential production logic quirk.
196
- const result = FileValidator.validateFileContent(Buffer.from('<html></html>'), 'text/html', [
197
- 'text/html'
198
- ]);
199
- expect(result.valid).toBe(false);
200
- expect(result.message).toContain('Unable to verify file type');
201
- });
202
- it('fails for binary content with no recognizable signature and no text fallback', ()=>{
203
- const result = FileValidator.validateFileContent(UNKNOWN_BYTES, 'application/octet-stream', [
204
- '*/*'
205
- ]);
206
- expect(result.valid).toBe(false);
207
- expect(result.message).toContain('Unable to verify file type');
208
- });
209
- it('defaults allowedTypes to */* when not provided', ()=>{
210
- const result = FileValidator.validateFileContent(JPEG_BYTES, 'image/jpeg');
211
- expect(result.valid).toBe(true);
212
- });
213
- it('returns a failure result instead of throwing on unexpected internal errors', ()=>{
214
- // Passing a non-buffer-like value to force detectFileType internals to misbehave gracefully
215
- const brokenBuffer = {
216
- length: 10
217
- };
218
- const result = FileValidator.validateFileContent(brokenBuffer, 'image/jpeg', [
219
- '*/*'
220
- ]);
221
- expect(result.valid).toBe(false);
222
- });
223
- });
224
- describe('sanitizeFilename', ()=>{
225
- it('strips directory components to prevent path traversal', ()=>{
226
- expect(FileValidator.sanitizeFilename('../../etc/passwd')).toBe('passwd');
227
- });
228
- it('strips Windows-style directory components', ()=>{
229
- expect(FileValidator.sanitizeFilename('..\\..\\windows\\system32\\evil.exe')).toBe('evil.exe');
230
- });
231
- it('removes null bytes', ()=>{
232
- expect(FileValidator.sanitizeFilename('file\0.txt')).toBe('file.txt');
233
- });
234
- it('collapses multiple consecutive dots into one', ()=>{
235
- expect(FileValidator.sanitizeFilename('archive....tar.gz')).toBe('archive.tar.gz');
236
- });
237
- it('replaces special/unsafe characters with underscores', ()=>{
238
- expect(FileValidator.sanitizeFilename('my file (final)!.txt')).toBe('my_file__final__.txt');
239
- });
240
- it('truncates filenames longer than 255 characters', ()=>{
241
- const longName = `${'a'.repeat(300)}.txt`;
242
- const sanitized = FileValidator.sanitizeFilename(longName);
243
- expect(sanitized.length).toBeLessThanOrEqual(255);
244
- });
245
- it('preserves a simple safe filename unchanged', ()=>{
246
- expect(FileValidator.sanitizeFilename('report-2024_final.pdf')).toBe('report-2024_final.pdf');
247
- });
248
- it('neutralizes an absolute-path traversal attempt embedded mid-string', ()=>{
249
- const malicious = '/var/www/../../etc/passwd';
250
- const sanitized = FileValidator.sanitizeFilename(malicious);
251
- expect(sanitized).not.toContain('/');
252
- expect(sanitized).not.toContain('..');
253
- });
254
- });
255
- });