@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 +530 -0
- package/index.js +2 -0
- package/package.json +122 -0
- package/src/adapters/express.js +86 -0
- package/src/adapters/fastify.js +105 -0
- package/src/adapters/index.js +3 -0
- package/src/adapters/nextjs.js +74 -0
- package/src/audit/logger.js +32 -0
- package/src/core/rateLimiter.js +55 -0
- package/src/core/sanitizer.js +62 -0
- package/src/core/validator.js +163 -0
- package/src/errors/UploadError.js +43 -0
- package/src/index.js +65 -0
- package/src/react/DropZone.jsx +146 -0
- package/src/react/FilePreview.jsx +152 -0
- package/src/react/ProgressBar.jsx +82 -0
- package/src/react/UploadButton.jsx +123 -0
- package/src/react/index.js +4 -0
- package/src/scanners/clamav.js +50 -0
- package/src/scanners/magicBytes.js +62 -0
- package/src/scanners/polyglot.js +58 -0
- package/src/scanners/virustotal.js +85 -0
- package/src/scanners/zipBomb.js +85 -0
- package/src/storage/cloudinary.js +56 -0
- package/src/storage/index.js +27 -0
- package/src/storage/local.js +39 -0
- package/src/storage/s3.js +62 -0
- package/types/index.d.ts +380 -0
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// fileguard — TypeScript definitions
|
|
2
|
+
// Works with Node.js >=18, React >=18, Express, Next.js App Router, Fastify
|
|
3
|
+
|
|
4
|
+
/// <reference types="node" />
|
|
5
|
+
|
|
6
|
+
// ── Error codes ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export type ErrorCode =
|
|
9
|
+
| 'FILE_TOO_LARGE'
|
|
10
|
+
| 'INVALID_EXTENSION'
|
|
11
|
+
| 'INVALID_MIME_TYPE'
|
|
12
|
+
| 'INVALID_MAGIC_BYTES'
|
|
13
|
+
| 'ZIP_BOMB_DETECTED'
|
|
14
|
+
| 'POLYGLOT_DETECTED'
|
|
15
|
+
| 'UNSAFE_FILENAME'
|
|
16
|
+
| 'VIRUS_DETECTED'
|
|
17
|
+
| 'RATE_LIMIT_EXCEEDED'
|
|
18
|
+
| 'STORAGE_ERROR'
|
|
19
|
+
|
|
20
|
+
// ── Result shapes ─────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface UploadData {
|
|
23
|
+
url: string | null
|
|
24
|
+
filename: string
|
|
25
|
+
size: number
|
|
26
|
+
mimeType: string
|
|
27
|
+
storage: 'local' | 's3' | 'cloudinary' | string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SuccessResult {
|
|
31
|
+
success: true
|
|
32
|
+
data: UploadData
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface FailResult {
|
|
36
|
+
success: false
|
|
37
|
+
error: ErrorCode
|
|
38
|
+
message: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type Result = SuccessResult | FailResult
|
|
42
|
+
|
|
43
|
+
export interface ValidationSuccess {
|
|
44
|
+
success: true
|
|
45
|
+
sanitizedFilename: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type ValidationResult = ValidationSuccess | FailResult
|
|
49
|
+
|
|
50
|
+
export interface ScanSuccess {
|
|
51
|
+
success: true
|
|
52
|
+
skipped?: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type ScanResult = ScanSuccess | FailResult
|
|
56
|
+
|
|
57
|
+
// ── Config ────────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export interface ScanConfig {
|
|
60
|
+
/** Always true — cannot be disabled. */
|
|
61
|
+
magicBytes?: boolean
|
|
62
|
+
/** Always true for archive types. */
|
|
63
|
+
zipBomb?: boolean
|
|
64
|
+
/** Always true. */
|
|
65
|
+
polyglot?: boolean
|
|
66
|
+
/** Opt-in: requires clamscan peer dep. Default: false. */
|
|
67
|
+
clamav?: boolean
|
|
68
|
+
/** Opt-in: requires a VirusTotal API key. Default: false. */
|
|
69
|
+
virustotal?: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface RateLimitConfig {
|
|
73
|
+
enabled?: boolean
|
|
74
|
+
maxUploads?: number
|
|
75
|
+
windowMs?: number
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface AuditConfig {
|
|
79
|
+
enabled?: boolean
|
|
80
|
+
logPath?: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ClamAVOptions {
|
|
84
|
+
clamdscan?: {
|
|
85
|
+
host?: string
|
|
86
|
+
port?: number
|
|
87
|
+
timeout?: number
|
|
88
|
+
localFallback?: boolean
|
|
89
|
+
path?: string
|
|
90
|
+
configFile?: string
|
|
91
|
+
multiscan?: boolean
|
|
92
|
+
reloadDb?: boolean
|
|
93
|
+
active?: boolean
|
|
94
|
+
bypassRest?: boolean
|
|
95
|
+
}
|
|
96
|
+
preference?: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface VirusTotalOptions {
|
|
100
|
+
apiKey: string
|
|
101
|
+
/** Milliseconds between poll attempts. Default: 5000. */
|
|
102
|
+
pollIntervalMs?: number
|
|
103
|
+
/** Maximum polling attempts before skipping. Default: 3. */
|
|
104
|
+
maxPolls?: number
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface FileguardConfig {
|
|
108
|
+
// File validation
|
|
109
|
+
maxFileSize?: number
|
|
110
|
+
allowedExtensions?: string[]
|
|
111
|
+
allowedMimeTypes?: string[]
|
|
112
|
+
sanitizeFilename?: boolean
|
|
113
|
+
scan?: ScanConfig
|
|
114
|
+
rateLimit?: RateLimitConfig
|
|
115
|
+
audit?: AuditConfig
|
|
116
|
+
|
|
117
|
+
// Storage
|
|
118
|
+
storage?: 'local' | 's3' | 'cloudinary'
|
|
119
|
+
|
|
120
|
+
// Local storage
|
|
121
|
+
localPath?: string
|
|
122
|
+
|
|
123
|
+
// S3 storage
|
|
124
|
+
bucket?: string
|
|
125
|
+
region?: string
|
|
126
|
+
prefix?: string
|
|
127
|
+
endpoint?: string
|
|
128
|
+
|
|
129
|
+
// Cloudinary storage
|
|
130
|
+
cloudName?: string
|
|
131
|
+
apiKey?: string
|
|
132
|
+
apiSecret?: string
|
|
133
|
+
resourceType?: 'auto' | 'image' | 'video' | 'raw'
|
|
134
|
+
folder?: string
|
|
135
|
+
|
|
136
|
+
// Optional scanner options
|
|
137
|
+
clamavOptions?: ClamAVOptions
|
|
138
|
+
virustotalOptions?: VirusTotalOptions
|
|
139
|
+
|
|
140
|
+
// Next.js adapter
|
|
141
|
+
fieldName?: string
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── File input ────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
export interface FileInput {
|
|
147
|
+
buffer: Buffer
|
|
148
|
+
filename: string
|
|
149
|
+
mimeType: string
|
|
150
|
+
size: number
|
|
151
|
+
sanitizedFilename?: string
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Errors ────────────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
export declare class UploadError extends Error {
|
|
157
|
+
readonly code: ErrorCode
|
|
158
|
+
constructor(code: ErrorCode, message: string)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export declare function failResult(code: ErrorCode, message: string): FailResult
|
|
162
|
+
export declare function successResult(data: UploadData): SuccessResult
|
|
163
|
+
|
|
164
|
+
// ── createGuard ───────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
export interface ProcessMeta {
|
|
167
|
+
/** User ID or IP address used for rate limiting. */
|
|
168
|
+
key?: string
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface Guard {
|
|
172
|
+
process(file: FileInput, meta?: ProcessMeta): Promise<Result>
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export declare function createGuard(config?: FileguardConfig): Guard
|
|
176
|
+
export { createGuard as fileguard }
|
|
177
|
+
|
|
178
|
+
// ── Core validation ───────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
export declare function validateFile(
|
|
181
|
+
file: FileInput,
|
|
182
|
+
config?: FileguardConfig
|
|
183
|
+
): Promise<ValidationResult>
|
|
184
|
+
|
|
185
|
+
export declare function checkFileSize(
|
|
186
|
+
size: number,
|
|
187
|
+
maxFileSize: number
|
|
188
|
+
): { success: true } | FailResult
|
|
189
|
+
|
|
190
|
+
export declare function checkExtension(
|
|
191
|
+
filename: string,
|
|
192
|
+
allowedExtensions: string[]
|
|
193
|
+
): { success: true } | FailResult
|
|
194
|
+
|
|
195
|
+
export declare function checkMimeType(
|
|
196
|
+
mimeType: string,
|
|
197
|
+
allowedMimeTypes: string[]
|
|
198
|
+
): { success: true } | FailResult
|
|
199
|
+
|
|
200
|
+
export declare function mergeConfig(userConfig?: Partial<FileguardConfig>): FileguardConfig
|
|
201
|
+
|
|
202
|
+
// ── Sanitizer ─────────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
export interface SanitizeResult {
|
|
205
|
+
sanitizedFilename: string
|
|
206
|
+
wasUnsafe: boolean
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export declare function sanitizeFilename(filename: string): SanitizeResult
|
|
210
|
+
|
|
211
|
+
// ── Rate limiter ──────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
export interface RateLimiter {
|
|
214
|
+
check(key: string): { success: true } | FailResult
|
|
215
|
+
reset(key: string): void
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export declare function createRateLimiter(
|
|
219
|
+
config?: Pick<RateLimitConfig, 'maxUploads' | 'windowMs'>
|
|
220
|
+
): RateLimiter
|
|
221
|
+
|
|
222
|
+
// ── Audit logger ──────────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
export interface Logger {
|
|
225
|
+
log(event: Record<string, unknown>): Promise<void>
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export declare function createLogger(config?: AuditConfig): Logger
|
|
229
|
+
|
|
230
|
+
// ── Scanners ──────────────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
export interface DetectedType {
|
|
233
|
+
ext: string
|
|
234
|
+
mime: string
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export declare function detectFileType(buffer: Buffer): Promise<DetectedType | null>
|
|
238
|
+
|
|
239
|
+
export declare function validateMagicBytes(
|
|
240
|
+
buffer: Buffer,
|
|
241
|
+
declaredMime: string,
|
|
242
|
+
allowedMimeTypes: string[]
|
|
243
|
+
): Promise<{ success: true; detectedMime: string; detectedExt: string } | FailResult>
|
|
244
|
+
|
|
245
|
+
export declare function checkZipBomb(
|
|
246
|
+
buffer: Buffer,
|
|
247
|
+
options?: { ratioThreshold?: number; maxFiles?: number }
|
|
248
|
+
): { success: true } | FailResult
|
|
249
|
+
|
|
250
|
+
export declare function checkPolyglot(buffer: Buffer): { success: true } | FailResult
|
|
251
|
+
|
|
252
|
+
export declare function scanWithClamAV(
|
|
253
|
+
buffer: Buffer,
|
|
254
|
+
clamavOptions?: ClamAVOptions
|
|
255
|
+
): Promise<ScanResult>
|
|
256
|
+
|
|
257
|
+
export declare function scanWithVirusTotal(
|
|
258
|
+
buffer: Buffer,
|
|
259
|
+
config?: VirusTotalOptions & { pollIntervalMs?: number; maxPolls?: number }
|
|
260
|
+
): Promise<ScanResult>
|
|
261
|
+
|
|
262
|
+
// ── Storage adapters ──────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
export interface LocalConfig {
|
|
265
|
+
localPath?: string
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface S3Config {
|
|
269
|
+
bucket: string
|
|
270
|
+
region?: string
|
|
271
|
+
prefix?: string
|
|
272
|
+
endpoint?: string
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface CloudinaryConfig {
|
|
276
|
+
cloudName: string
|
|
277
|
+
apiKey: string
|
|
278
|
+
apiSecret: string
|
|
279
|
+
resourceType?: 'auto' | 'image' | 'video' | 'raw'
|
|
280
|
+
folder?: string
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export declare function localStore(file: FileInput, config?: LocalConfig): Promise<Result>
|
|
284
|
+
export declare function s3Store(file: FileInput, config: S3Config): Promise<Result>
|
|
285
|
+
export declare function cloudinaryStore(file: FileInput, config: CloudinaryConfig): Promise<Result>
|
|
286
|
+
|
|
287
|
+
export type StorageAdapter = typeof localStore | typeof s3Store | typeof cloudinaryStore
|
|
288
|
+
|
|
289
|
+
export declare function getStorageAdapter(
|
|
290
|
+
type: 'local' | 's3' | 'cloudinary'
|
|
291
|
+
): Promise<StorageAdapter>
|
|
292
|
+
|
|
293
|
+
// ── Framework adapters ────────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
// Express
|
|
296
|
+
export interface ExpressRequest {
|
|
297
|
+
headers: Record<string, string | string[] | undefined>
|
|
298
|
+
pipe<T>(destination: T): T
|
|
299
|
+
uploadResult?: Result
|
|
300
|
+
[key: string]: unknown
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface ExpressResponse {
|
|
304
|
+
[key: string]: unknown
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export type NextFunction = (err?: unknown) => void
|
|
308
|
+
|
|
309
|
+
export declare function createExpressMiddleware(
|
|
310
|
+
config?: FileguardConfig
|
|
311
|
+
): (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => void
|
|
312
|
+
|
|
313
|
+
// Next.js App Router
|
|
314
|
+
export declare function createNextHandler(
|
|
315
|
+
config?: FileguardConfig
|
|
316
|
+
): (request: Request) => Promise<Response>
|
|
317
|
+
|
|
318
|
+
// Fastify
|
|
319
|
+
export declare function createFastifyPlugin(
|
|
320
|
+
config?: FileguardConfig
|
|
321
|
+
): (fastify: unknown) => Promise<void>
|
|
322
|
+
|
|
323
|
+
// ── React UI components ───────────────────────────────────────────────────────
|
|
324
|
+
// React (>=18) must be installed as a peer dependency.
|
|
325
|
+
|
|
326
|
+
export interface UploadError {
|
|
327
|
+
error: ErrorCode
|
|
328
|
+
message: string
|
|
329
|
+
file: File
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export interface DropZoneProps {
|
|
333
|
+
onUpload?: (file: File) => void
|
|
334
|
+
onError?: (err: UploadError) => void
|
|
335
|
+
accept?: string[]
|
|
336
|
+
maxSize?: number
|
|
337
|
+
headless?: boolean
|
|
338
|
+
multiple?: boolean
|
|
339
|
+
children?: unknown
|
|
340
|
+
className?: string
|
|
341
|
+
style?: Record<string, string | number>
|
|
342
|
+
[prop: string]: unknown
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export interface UploadButtonProps {
|
|
346
|
+
onUpload?: (file: File) => void
|
|
347
|
+
onError?: (err: UploadError) => void
|
|
348
|
+
accept?: string[]
|
|
349
|
+
maxSize?: number
|
|
350
|
+
multiple?: boolean
|
|
351
|
+
headless?: boolean
|
|
352
|
+
disabled?: boolean
|
|
353
|
+
children?: unknown
|
|
354
|
+
className?: string
|
|
355
|
+
style?: Record<string, string | number>
|
|
356
|
+
[prop: string]: unknown
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export interface ProgressBarProps {
|
|
360
|
+
progress?: number
|
|
361
|
+
label?: string
|
|
362
|
+
headless?: boolean
|
|
363
|
+
className?: string
|
|
364
|
+
style?: Record<string, string | number>
|
|
365
|
+
[prop: string]: unknown
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export interface FilePreviewProps {
|
|
369
|
+
file: File | null
|
|
370
|
+
onRemove?: () => void
|
|
371
|
+
headless?: boolean
|
|
372
|
+
className?: string
|
|
373
|
+
style?: Record<string, string | number>
|
|
374
|
+
[prop: string]: unknown
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export declare function DropZone(props: DropZoneProps): unknown
|
|
378
|
+
export declare function UploadButton(props: UploadButtonProps): unknown
|
|
379
|
+
export declare function ProgressBar(props: ProgressBarProps): unknown
|
|
380
|
+
export declare function FilePreview(props: FilePreviewProps): unknown
|