@msal95/fileguard 0.1.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/README.md ADDED
@@ -0,0 +1,530 @@
1
+ # fileguard
2
+
3
+ Production-grade secure file upload middleware for Node.js.
4
+
5
+ Not just a file picker โ€” real security: magic byte detection, ZIP bomb protection, polyglot file blocking, optional ClamAV + VirusTotal scanning, and unified adapters for Express, Next.js, and Fastify.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@msal95/fileguard.svg)](https://www.npmjs.com/package/@msal95/fileguard)
8
+ [![npm downloads](https://img.shields.io/npm/dm/@msal95/fileguard.svg)](https://www.npmjs.com/package/@msal95/fileguard)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
+ [![Node >=18](https://img.shields.io/node/v/@msal95/fileguard.svg)](https://www.npmjs.com/package/@msal95/fileguard)
11
+
12
+ ---
13
+
14
+ ## Features
15
+
16
+ - ๐Ÿ” **Magic bytes validation** โ€” reads the actual file signature, not just the extension or declared MIME type
17
+ - ๐Ÿ’ฃ **ZIP bomb detection** โ€” rejects archives with compression ratio > 100ร— or > 1000 files
18
+ - ๐Ÿงฌ **Polyglot detection** โ€” blocks files that embed MZ/EXE, `<script>`, PHP, nested ZIP, or shell shebangs
19
+ - ๐Ÿงน **Filename sanitization** โ€” strips path traversal, null bytes, reserved names, and unsafe characters
20
+ - ๐Ÿšฆ **Rate limiting** โ€” per-user/IP in-memory limiter, no Redis required
21
+ - ๐Ÿฆ  **ClamAV scanning** โ€” opt-in, skips gracefully if daemon is unavailable
22
+ - ๐ŸŒ **VirusTotal scanning** โ€” opt-in, skips gracefully on missing API key or network failure
23
+ - ๐Ÿ—„๏ธ **Storage adapters** โ€” local disk, AWS S3, Cloudinary
24
+ - โšก **Framework adapters** โ€” Express middleware, Next.js App Router handler, Fastify plugin
25
+ - โš›๏ธ **React UI components** โ€” DropZone, UploadButton, ProgressBar, FilePreview with CSS variable theming
26
+ - ๐Ÿ“‹ **Audit logging** โ€” append-only JSON log of every upload attempt
27
+ - ๐ŸŸฆ **TypeScript** โ€” full type definitions included, no `@types/fileguard` needed
28
+
29
+ ---
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ npm install @msal95/fileguard
35
+ yarn add @msal95/fileguard
36
+ pnpm add @msal95/fileguard
37
+ bun add @msal95/fileguard
38
+ ```
39
+
40
+ Optional peer dependencies โ€” install only what you need:
41
+
42
+ ```bash
43
+ # S3 storage
44
+ npm install @aws-sdk/client-s3
45
+ yarn add @aws-sdk/client-s3
46
+
47
+ # Cloudinary storage
48
+ npm install cloudinary
49
+ yarn add cloudinary
50
+
51
+ # ClamAV scanning
52
+ npm install clamscan
53
+ yarn add clamscan
54
+
55
+ # React UI components
56
+ npm install react
57
+ yarn add react
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Validation Pipeline
63
+
64
+ Every upload passes through this fixed sequence. No step can be skipped.
65
+
66
+ | Step | Check |
67
+ |------|-------|
68
+ | 1 | File size |
69
+ | 2 | Extension allowlist |
70
+ | 3 | MIME type allowlist |
71
+ | 4 | Magic bytes (reads first 8 KB of buffer) |
72
+ | 5 | ZIP bomb detection (archive types only) |
73
+ | 6 | Polyglot detection |
74
+ | 7 | Filename sanitization |
75
+ | 8 | ClamAV scan *(opt-in)* |
76
+ | 9 | VirusTotal scan *(opt-in)* |
77
+ | 10 | Rate limit check |
78
+ | 11 | Store to adapter |
79
+
80
+ ---
81
+
82
+ ## Quick Start
83
+
84
+ ```js
85
+ import { createGuard } from '@msal95/fileguard'
86
+
87
+ const guard = createGuard({
88
+ allowedExtensions: ['jpg', 'png', 'pdf'],
89
+ allowedMimeTypes: ['image/jpeg', 'image/png', 'application/pdf'],
90
+ maxFileSize: 5 * 1024 * 1024, // 5 MB
91
+ storage: 'local',
92
+ localPath: './uploads',
93
+ })
94
+
95
+ const result = await guard.process({
96
+ buffer,
97
+ filename: 'photo.jpg',
98
+ mimeType: 'image/jpeg',
99
+ size: buffer.length,
100
+ })
101
+
102
+ if (result.success) {
103
+ console.log(result.data.url)
104
+ } else {
105
+ console.error(result.error, result.message)
106
+ }
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Express
112
+
113
+ ```js
114
+ import express from 'express'
115
+ import { createExpressMiddleware } from '@msal95/fileguard/express'
116
+
117
+ const app = express()
118
+
119
+ app.post(
120
+ '/upload',
121
+ createExpressMiddleware({
122
+ allowedExtensions: ['jpg', 'png', 'pdf'],
123
+ allowedMimeTypes: ['image/jpeg', 'image/png', 'application/pdf'],
124
+ maxFileSize: 5 * 1024 * 1024,
125
+ storage: 'local',
126
+ localPath: './uploads',
127
+ }),
128
+ (req, res) => {
129
+ if (!req.uploadResult.success) return res.status(422).json(req.uploadResult)
130
+ res.json(req.uploadResult)
131
+ }
132
+ )
133
+ ```
134
+
135
+ The middleware always calls `next()`. Validation errors appear in `req.uploadResult` โ€” nothing is ever thrown.
136
+
137
+ ---
138
+
139
+ ## Next.js App Router
140
+
141
+ ```js
142
+ // app/api/upload/route.js
143
+ import { createNextHandler } from '@msal95/fileguard/nextjs'
144
+
145
+ export const POST = createNextHandler({
146
+ allowedExtensions: ['jpg', 'png', 'pdf'],
147
+ allowedMimeTypes: ['image/jpeg', 'image/png', 'application/pdf'],
148
+ maxFileSize: 5 * 1024 * 1024,
149
+ storage: 'local',
150
+ localPath: './public/uploads',
151
+ fieldName: 'file', // default
152
+ })
153
+ ```
154
+
155
+ Returns a `Response` with JSON. Status `200` on success, `422` on validation failure, `400` when no file found.
156
+
157
+ ---
158
+
159
+ ## Fastify
160
+
161
+ ```js
162
+ import Fastify from 'fastify'
163
+ import { createFastifyPlugin } from '@msal95/fileguard/fastify'
164
+
165
+ const fastify = Fastify()
166
+
167
+ await fastify.register(createFastifyPlugin({
168
+ storage: 'local',
169
+ localPath: './uploads',
170
+ }))
171
+
172
+ fastify.post('/upload', { preHandler: fastify.uploadGuard() }, async (req, reply) => {
173
+ return req.uploadResult
174
+ })
175
+ ```
176
+
177
+ ---
178
+
179
+ ## Storage Adapters
180
+
181
+ ### Local
182
+
183
+ ```js
184
+ { storage: 'local', localPath: './uploads' }
185
+ ```
186
+
187
+ ### S3
188
+
189
+ ```bash
190
+ npm install @aws-sdk/client-s3
191
+ ```
192
+
193
+ ```js
194
+ {
195
+ storage: 's3',
196
+ bucket: 'my-bucket',
197
+ region: 'us-east-1',
198
+ prefix: 'uploads', // optional key prefix
199
+ endpoint: '...', // optional โ€” for S3-compatible services (MinIO, R2, etc.)
200
+ }
201
+ ```
202
+
203
+ ### Cloudinary
204
+
205
+ ```bash
206
+ npm install cloudinary
207
+ ```
208
+
209
+ ```js
210
+ {
211
+ storage: 'cloudinary',
212
+ cloudName: 'my-cloud',
213
+ apiKey: 'key',
214
+ apiSecret: 'secret',
215
+ resourceType: 'auto', // 'image' | 'video' | 'raw' | 'auto'
216
+ folder: 'uploads', // optional
217
+ }
218
+ ```
219
+
220
+ ---
221
+
222
+ ## Optional Scanners
223
+
224
+ ### ClamAV
225
+
226
+ ```bash
227
+ npm install clamscan
228
+ ```
229
+
230
+ ```js
231
+ {
232
+ scan: { clamav: true },
233
+ clamavOptions: {
234
+ clamdscan: { host: '127.0.0.1', port: 3310 },
235
+ },
236
+ }
237
+ ```
238
+
239
+ If `clamscan` is not installed or the daemon is unreachable, the scan is **skipped with a console warning** โ€” the upload is never blocked by a missing scanner.
240
+
241
+ ### VirusTotal
242
+
243
+ ```js
244
+ {
245
+ scan: { virustotal: true },
246
+ virustotalOptions: {
247
+ apiKey: process.env.VT_API_KEY,
248
+ pollIntervalMs: 5000, // default
249
+ maxPolls: 3, // default
250
+ },
251
+ }
252
+ ```
253
+
254
+ If the API key is missing or the request fails, the scan is **skipped** โ€” the upload proceeds.
255
+
256
+ ---
257
+
258
+ ## React UI Components
259
+
260
+ ```bash
261
+ npm install react
262
+ ```
263
+
264
+ ```jsx
265
+ import { DropZone, UploadButton, ProgressBar, FilePreview } from '@msal95/fileguard/react'
266
+
267
+ function Uploader() {
268
+ const [file, setFile] = useState(null)
269
+ const [progress, setProgress] = useState(0)
270
+
271
+ const handleUpload = async (file) => {
272
+ setFile(file)
273
+ const form = new FormData()
274
+ form.append('file', file)
275
+
276
+ const xhr = new XMLHttpRequest()
277
+ xhr.upload.onprogress = (e) => setProgress(Math.round(e.loaded / e.total * 100))
278
+ xhr.open('POST', '/api/upload')
279
+ xhr.send(form)
280
+ }
281
+
282
+ return (
283
+ <>
284
+ <DropZone
285
+ onUpload={handleUpload}
286
+ onError={(err) => console.error(err.message)}
287
+ accept={['jpg', 'png', 'pdf']}
288
+ maxSize={5 * 1024 * 1024}
289
+ />
290
+ {file && <FilePreview file={file} onRemove={() => setFile(null)} />}
291
+ {progress > 0 && <ProgressBar progress={progress} label="Uploadingโ€ฆ" />}
292
+ <UploadButton onUpload={handleUpload}>Pick a file</UploadButton>
293
+ </>
294
+ )
295
+ }
296
+ ```
297
+
298
+ All four components accept a `headless` prop โ€” set it to `true` to strip all built-in styles and apply your own CSS.
299
+
300
+ ### CSS Variables
301
+
302
+ Theme any component by setting these on a parent element:
303
+
304
+ ```css
305
+ :root {
306
+ --fg-primary: #2563eb;
307
+ --fg-border: #d1d5db;
308
+ --fg-bg: #fafafa;
309
+ --fg-bg-active: #eff6ff;
310
+ --fg-text: #111827;
311
+ --fg-text-muted: #9ca3af;
312
+ --fg-radius: 8px;
313
+ --fg-padding: 40px 24px;
314
+ --fg-font-size: 14px;
315
+ --fg-bar-height: 8px;
316
+ --fg-bar-bg: #e5e7eb;
317
+ --fg-btn-padding: 8px 18px;
318
+ --fg-btn-text: #ffffff;
319
+ }
320
+ ```
321
+
322
+ ---
323
+
324
+ ## Rate Limiting
325
+
326
+ ```js
327
+ import { createGuard } from '@msal95/fileguard'
328
+
329
+ const guard = createGuard({
330
+ storage: 'local',
331
+ localPath: './uploads',
332
+ rateLimit: {
333
+ enabled: true,
334
+ maxUploads: 10, // per key per window
335
+ windowMs: 60_000, // 1 minute
336
+ },
337
+ })
338
+
339
+ // Pass a key (user ID or IP) as the second argument
340
+ const result = await guard.process(file, { key: req.ip })
341
+ ```
342
+
343
+ ---
344
+
345
+ ## Audit Logging
346
+
347
+ ```js
348
+ {
349
+ audit: {
350
+ enabled: true,
351
+ logPath: './logs/uploads.log', // appends JSON-newline entries
352
+ },
353
+ }
354
+ ```
355
+
356
+ Each log entry contains: `event`, `filename`, `size`, `storage`, `url` or `error`, and a UTC timestamp.
357
+
358
+ ---
359
+
360
+ ## Low-Level API
361
+
362
+ Use the building blocks directly without a framework adapter:
363
+
364
+ ```js
365
+ import { validateFile } from '@msal95/fileguard'
366
+ import { localStore } from '@msal95/fileguard/storage/local'
367
+
368
+ const validation = await validateFile(
369
+ { buffer, filename: 'photo.png', mimeType: 'image/png', size: buffer.length },
370
+ { allowedExtensions: ['png'], allowedMimeTypes: ['image/png'] }
371
+ )
372
+
373
+ if (!validation.success) {
374
+ console.error(validation.error) // 'INVALID_EXTENSION' | 'INVALID_MAGIC_BYTES' | โ€ฆ
375
+ } else {
376
+ const result = await localStore(
377
+ { ...file, sanitizedFilename: validation.sanitizedFilename },
378
+ { localPath: './uploads' }
379
+ )
380
+ }
381
+ ```
382
+
383
+ ---
384
+
385
+ ## Result Shape
386
+
387
+ Every function returns a plain object โ€” nothing is ever thrown to the caller.
388
+
389
+ ```js
390
+ // Success
391
+ { success: true, data: { url, filename, size, mimeType, storage } }
392
+
393
+ // Failure
394
+ { success: false, error: 'ERROR_CODE', message: 'Human readable message' }
395
+ ```
396
+
397
+ ### Error Codes
398
+
399
+ | Code | Trigger |
400
+ |------|---------|
401
+ | `FILE_TOO_LARGE` | File exceeds `maxFileSize` |
402
+ | `INVALID_EXTENSION` | Extension not in `allowedExtensions` |
403
+ | `INVALID_MIME_TYPE` | Declared MIME not in `allowedMimeTypes` |
404
+ | `INVALID_MAGIC_BYTES` | File content doesn't match declared type |
405
+ | `ZIP_BOMB_DETECTED` | Compression ratio > 100ร— or > 1000 files |
406
+ | `POLYGLOT_DETECTED` | File embeds MZ, `<script>`, nested ZIP, or PHP |
407
+ | `UNSAFE_FILENAME` | Reserved for future use |
408
+ | `VIRUS_DETECTED` | ClamAV or VirusTotal flagged the file |
409
+ | `RATE_LIMIT_EXCEEDED` | Upload rate limit exceeded |
410
+ | `STORAGE_ERROR` | Write to storage adapter failed |
411
+
412
+ ---
413
+
414
+ ## Default Configuration
415
+
416
+ ```js
417
+ {
418
+ maxFileSize: 10 * 1024 * 1024, // 10 MB
419
+ allowedExtensions: [
420
+ 'jpg', 'jpeg', 'png', 'gif', 'webp',
421
+ 'pdf', 'doc', 'docx', 'xls', 'xlsx',
422
+ ],
423
+ allowedMimeTypes: [
424
+ 'image/jpeg', 'image/png', 'image/gif',
425
+ 'image/webp', 'application/pdf',
426
+ ],
427
+ storage: 'local',
428
+ localPath: './uploads',
429
+ scan: {
430
+ magicBytes: true, // always on โ€” cannot be disabled
431
+ zipBomb: true, // runs for archive types
432
+ polyglot: true, // always on
433
+ clamav: false, // opt-in
434
+ virustotal: false, // opt-in
435
+ },
436
+ rateLimit: {
437
+ enabled: false,
438
+ maxUploads: 10,
439
+ windowMs: 60_000,
440
+ },
441
+ audit: {
442
+ enabled: false,
443
+ logPath: './logs/uploads.log',
444
+ },
445
+ sanitizeFilename: true,
446
+ }
447
+ ```
448
+
449
+ ---
450
+
451
+ ## TypeScript
452
+
453
+ Full type definitions are included โ€” no `@types/fileguard` needed.
454
+
455
+ ```ts
456
+ import { createGuard } from '@msal95/fileguard'
457
+ import type { FileguardConfig, Result } from '@msal95/fileguard'
458
+
459
+ const guard = createGuard({ storage: 'local', localPath: './uploads' })
460
+
461
+ const result: Result = await guard.process({
462
+ buffer,
463
+ filename: 'photo.jpg',
464
+ mimeType: 'image/jpeg',
465
+ size: buffer.length,
466
+ })
467
+
468
+ if (result.success) {
469
+ console.log(result.data.url)
470
+ } else {
471
+ console.error(result.error, result.message)
472
+ }
473
+ ```
474
+
475
+ ---
476
+
477
+ ## Sub-path Exports
478
+
479
+ ```js
480
+ import { createGuard, validateFile } from '@msal95/fileguard'
481
+ import { createExpressMiddleware } from '@msal95/fileguard/express'
482
+ import { createNextHandler } from '@msal95/fileguard/nextjs'
483
+ import { createFastifyPlugin } from '@msal95/fileguard/fastify'
484
+ import { localStore } from '@msal95/fileguard/storage/local'
485
+ import { s3Store } from '@msal95/fileguard/storage/s3'
486
+ import { cloudinaryStore } from '@msal95/fileguard/storage/cloudinary'
487
+ import { DropZone, UploadButton } from '@msal95/fileguard/react'
488
+ import { scanWithClamAV } from '@msal95/fileguard/scanners/clamav'
489
+ import { scanWithVirusTotal } from '@msal95/fileguard/scanners/virustotal'
490
+ ```
491
+
492
+ ---
493
+
494
+ ## What's Built
495
+
496
+ | Module | Status |
497
+ |--------|--------|
498
+ | Core validation (size, extension, MIME, magic bytes) | โœ… |
499
+ | ZIP bomb detection | โœ… |
500
+ | Polyglot file detection | โœ… |
501
+ | Filename sanitization | โœ… |
502
+ | In-memory rate limiter | โœ… |
503
+ | Audit logger | โœ… |
504
+ | Local storage adapter | โœ… |
505
+ | S3 storage adapter | โœ… |
506
+ | Cloudinary storage adapter | โœ… |
507
+ | Express middleware | โœ… |
508
+ | Next.js App Router handler | โœ… |
509
+ | Fastify plugin | โœ… |
510
+ | ClamAV scanner (opt-in) | โœ… |
511
+ | VirusTotal scanner (opt-in) | โœ… |
512
+ | React UI components (4 components) | โœ… |
513
+ | TypeScript definitions | โœ… |
514
+
515
+ ---
516
+
517
+ ## Requirements
518
+
519
+ - Node.js >= 18.0.0
520
+ - `file-type`, `busboy`, `uuid` (included as dependencies)
521
+ - `@aws-sdk/client-s3` (optional โ€” S3 storage)
522
+ - `cloudinary` (optional โ€” Cloudinary storage)
523
+ - `clamscan` (optional โ€” ClamAV scanning)
524
+ - `react >= 18` (optional โ€” UI components)
525
+
526
+ ---
527
+
528
+ ## License
529
+
530
+ MIT ยฉ [Muhammad Shahid](https://github.com/msal95)
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createGuard, fileguard } from './src/index.js'
2
+ export { UploadError } from './src/errors/UploadError.js'
package/package.json ADDED
@@ -0,0 +1,122 @@
1
+ {
2
+ "name": "@msal95/fileguard",
3
+ "version": "0.1.1",
4
+ "description": "Production-grade secure file upload middleware for Node.js โ€” magic bytes, ZIP bomb & polyglot detection, virus scanning, multi-storage, and React UI components",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "types": "./types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./index.js",
11
+ "types": "./types/index.d.ts"
12
+ },
13
+ "./express": {
14
+ "import": "./src/adapters/express.js",
15
+ "types": "./types/index.d.ts"
16
+ },
17
+ "./nextjs": {
18
+ "import": "./src/adapters/nextjs.js",
19
+ "types": "./types/index.d.ts"
20
+ },
21
+ "./fastify": {
22
+ "import": "./src/adapters/fastify.js",
23
+ "types": "./types/index.d.ts"
24
+ },
25
+ "./storage/local": {
26
+ "import": "./src/storage/local.js",
27
+ "types": "./types/index.d.ts"
28
+ },
29
+ "./storage/s3": {
30
+ "import": "./src/storage/s3.js",
31
+ "types": "./types/index.d.ts"
32
+ },
33
+ "./storage/cloudinary": {
34
+ "import": "./src/storage/cloudinary.js",
35
+ "types": "./types/index.d.ts"
36
+ },
37
+ "./react": {
38
+ "import": "./src/react/index.js",
39
+ "types": "./types/index.d.ts"
40
+ },
41
+ "./scanners/clamav": {
42
+ "import": "./src/scanners/clamav.js",
43
+ "types": "./types/index.d.ts"
44
+ },
45
+ "./scanners/virustotal": {
46
+ "import": "./src/scanners/virustotal.js",
47
+ "types": "./types/index.d.ts"
48
+ }
49
+ },
50
+ "files": [
51
+ "index.js",
52
+ "src/",
53
+ "types/"
54
+ ],
55
+ "engines": {
56
+ "node": ">=18.0.0"
57
+ },
58
+ "scripts": {
59
+ "test": "vitest run",
60
+ "test:watch": "vitest",
61
+ "typecheck": "tsc --noEmit"
62
+ },
63
+ "keywords": [
64
+ "file-upload",
65
+ "security",
66
+ "middleware",
67
+ "express",
68
+ "nextjs",
69
+ "fastify",
70
+ "clamav",
71
+ "virustotal",
72
+ "magic-bytes",
73
+ "zip-bomb",
74
+ "polyglot",
75
+ "s3",
76
+ "cloudinary",
77
+ "react",
78
+ "typescript",
79
+ "multipart",
80
+ "upload-validation"
81
+ ],
82
+ "author": "Muhammad Shahid",
83
+ "license": "MIT",
84
+ "repository": {
85
+ "type": "git",
86
+ "url": "git+https://github.com/msal95/fileguard.git"
87
+ },
88
+ "homepage": "https://github.com/msal95/fileguard#readme",
89
+ "bugs": {
90
+ "url": "https://github.com/msal95/fileguard/issues"
91
+ },
92
+ "dependencies": {
93
+ "busboy": "^1.6.0",
94
+ "file-type": "^19.0.0",
95
+ "uuid": "^9.0.0"
96
+ },
97
+ "devDependencies": {
98
+ "@types/node": "^25.8.0",
99
+ "typescript": "^5.0.0",
100
+ "vitest": "^1.6.0"
101
+ },
102
+ "peerDependencies": {
103
+ "@aws-sdk/client-s3": "^3.0.0",
104
+ "clamscan": "^2.0.0",
105
+ "cloudinary": "^2.0.0",
106
+ "react": ">=18.0.0"
107
+ },
108
+ "peerDependenciesMeta": {
109
+ "@aws-sdk/client-s3": {
110
+ "optional": true
111
+ },
112
+ "cloudinary": {
113
+ "optional": true
114
+ },
115
+ "clamscan": {
116
+ "optional": true
117
+ },
118
+ "react": {
119
+ "optional": true
120
+ }
121
+ }
122
+ }