@arcis/node 1.0.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 (52) hide show
  1. package/README.md +222 -0
  2. package/dist/core/index.d.mts +170 -0
  3. package/dist/core/index.d.ts +170 -0
  4. package/dist/core/index.js +327 -0
  5. package/dist/core/index.js.map +1 -0
  6. package/dist/core/index.mjs +307 -0
  7. package/dist/core/index.mjs.map +1 -0
  8. package/dist/headers-BJq2OA0i.d.ts +284 -0
  9. package/dist/headers-DBQedhrb.d.mts +284 -0
  10. package/dist/index-BgHPM7LC.d.ts +129 -0
  11. package/dist/index-BpT7flAQ.d.ts +255 -0
  12. package/dist/index-JaFOUKyK.d.mts +255 -0
  13. package/dist/index-nAgXexwD.d.mts +129 -0
  14. package/dist/index.d.mts +139 -0
  15. package/dist/index.d.ts +139 -0
  16. package/dist/index.js +1860 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/index.mjs +1797 -0
  19. package/dist/index.mjs.map +1 -0
  20. package/dist/logging/index.d.mts +38 -0
  21. package/dist/logging/index.d.ts +38 -0
  22. package/dist/logging/index.js +140 -0
  23. package/dist/logging/index.js.map +1 -0
  24. package/dist/logging/index.mjs +136 -0
  25. package/dist/logging/index.mjs.map +1 -0
  26. package/dist/middleware/index.d.mts +3 -0
  27. package/dist/middleware/index.d.ts +3 -0
  28. package/dist/middleware/index.js +1173 -0
  29. package/dist/middleware/index.js.map +1 -0
  30. package/dist/middleware/index.mjs +1156 -0
  31. package/dist/middleware/index.mjs.map +1 -0
  32. package/dist/sanitizers/index.d.mts +24 -0
  33. package/dist/sanitizers/index.d.ts +24 -0
  34. package/dist/sanitizers/index.js +610 -0
  35. package/dist/sanitizers/index.js.map +1 -0
  36. package/dist/sanitizers/index.mjs +587 -0
  37. package/dist/sanitizers/index.mjs.map +1 -0
  38. package/dist/stores/index.d.mts +106 -0
  39. package/dist/stores/index.d.ts +106 -0
  40. package/dist/stores/index.js +149 -0
  41. package/dist/stores/index.js.map +1 -0
  42. package/dist/stores/index.mjs +145 -0
  43. package/dist/stores/index.mjs.map +1 -0
  44. package/dist/types-BOdL3ZWo.d.mts +264 -0
  45. package/dist/types-BOdL3ZWo.d.ts +264 -0
  46. package/dist/validation/index.d.mts +3 -0
  47. package/dist/validation/index.d.ts +3 -0
  48. package/dist/validation/index.js +705 -0
  49. package/dist/validation/index.js.map +1 -0
  50. package/dist/validation/index.mjs +699 -0
  51. package/dist/validation/index.mjs.map +1 -0
  52. package/package.json +109 -0
@@ -0,0 +1,705 @@
1
+ 'use strict';
2
+
3
+ // src/core/constants.ts
4
+ var INPUT = {
5
+ /** Default maximum input size (1MB) */
6
+ DEFAULT_MAX_SIZE: 1e6};
7
+ var XSS_REMOVE_PATTERNS = [
8
+ /** Full script blocks (content + tags) */
9
+ /<script[^>]*>[\s\S]*?<\/script>/gi,
10
+ /** Standalone/unclosed script tags */
11
+ /<script[^>]*>/gi,
12
+ /** iframe — full block and partial/unclosed */
13
+ /<iframe[^>]*>[\s\S]*?<\/iframe>/gi,
14
+ /<iframe[^>]*/gi,
15
+ /** object — full block and partial/unclosed */
16
+ /<object[^>]*>[\s\S]*?<\/object>/gi,
17
+ /<object[^>]*/gi,
18
+ /** embed tags */
19
+ /<embed[^>]*/gi,
20
+ /** SVG with inline event handlers */
21
+ /<svg[^>]*onload[^>]*>/gi,
22
+ /** URL-encoded script tags */
23
+ /%3Cscript/gi,
24
+ /** Event handlers with quoted values: onclick="...", onerror='...' */
25
+ /(?:[\s/])on\w+\s*=\s*["'][^"']*["']/gi,
26
+ /** Event handlers with unquoted values: onload=value */
27
+ /(?:[\s/])on\w+\s*=\s*[^\s>]*/gi,
28
+ /** javascript: and vbscript: protocols (allow optional spaces before colon) */
29
+ /javascript\s*:/gi,
30
+ /vbscript\s*:/gi,
31
+ /** data: URIs with HTML/script content */
32
+ /data\s*:\s*text\/html[^>\s]*/gi
33
+ ];
34
+ var SQL_PATTERNS = [
35
+ /** SQL keywords */
36
+ /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE)\b)/gi,
37
+ /** SQL comments: ANSI (--), C-style (slash-star ... star-slash), MySQL (#) */
38
+ /(--|\/\*|\*\/|#)/g,
39
+ /** SQL statement separators */
40
+ /(;|\|\||&&)/g,
41
+ /** Boolean injection: OR 1=1 */
42
+ /\bOR\s+\d+\s*=\s*\d+/gi,
43
+ /** Boolean injection: OR 'a'='a' or OR "a"="a" (including mixed quotes) */
44
+ /\bOR\s+(['"])[^'"]*\1\s*=\s*(['"])[^'"]*\2/gi,
45
+ /\bOR\s+('[^']*'|"[^"]*")\s*=\s*('[^']*'|"[^"]*")/gi,
46
+ /** Boolean injection: AND 1=1 */
47
+ /\bAND\s+\d+\s*=\s*\d+/gi,
48
+ /** Boolean injection: AND 'a'='a' or AND "a"="a" (including mixed quotes) */
49
+ /\bAND\s+(['"])[^'"]*\1\s*=\s*(['"])[^'"]*\2/gi,
50
+ /\bAND\s+('[^']*'|"[^"]*")\s*=\s*('[^']*'|"[^"]*")/gi,
51
+ /** Time-based blind: SLEEP() */
52
+ /\bSLEEP\s*\(\s*\d+\s*\)/gi,
53
+ /** Time-based blind: BENCHMARK() */
54
+ /\bBENCHMARK\s*\(/gi
55
+ ];
56
+ var PATH_PATTERNS = [
57
+ /** Unix path traversal */
58
+ /\.\.\//g,
59
+ /** Windows path traversal */
60
+ /\.\.\\/g,
61
+ /** URL-encoded traversal (%2e%2e) */
62
+ /%2e%2e/gi,
63
+ /** Double URL-encoded traversal (%252e) */
64
+ /%252e/gi,
65
+ /** Mixed encoding: ..%2F */
66
+ /\.\.%2F/gi,
67
+ /** Mixed encoding: %2e./ and .%2e/ */
68
+ /%2e\.[\\/]/gi,
69
+ /\.%2e[\\/]/gi,
70
+ /** Fully URL-encoded: %2e%2e%2f */
71
+ /%2e%2e%2f/gi,
72
+ /** Null byte injection in paths */
73
+ /\0/g
74
+ ];
75
+ var COMMAND_PATTERNS = [
76
+ /**
77
+ * Shell metacharacters that enable command chaining/substitution.
78
+ * Bare ( and ) are excluded — they appear in common legitimate values
79
+ * (function calls in code fields, math expressions, etc.).
80
+ * Command substitution is caught by the $( combined pattern below.
81
+ * NOTE: ';', '&', '|' may appear in legitimate URL query strings
82
+ * and Markdown; consider disabling command checking (command: false)
83
+ * for fields that intentionally allow those characters.
84
+ */
85
+ /[;&|`]/g,
86
+ /** Command substitution: $( ... ) — matched as a pair to reduce false positives */
87
+ /\$\(/g
88
+ ];
89
+ var VALIDATION = {
90
+ /**
91
+ * Email regex pattern.
92
+ * Rejects consecutive dots in local part (e.g. test..foo@example.com),
93
+ * leading/trailing dots, and other common invalid forms.
94
+ */
95
+ EMAIL: /^[^\s@.][^\s@]*(?:\.[^\s@.][^\s@]*)*@[^\s@]+\.[^\s@]+$/,
96
+ /**
97
+ * URL regex pattern.
98
+ * Only allows http:// and https:// — explicitly rejects javascript:,
99
+ * data:, vbscript:, and other dangerous URI schemes.
100
+ */
101
+ URL: /^https?:\/\/[^\s/$.?#][^\s]*$/,
102
+ /** UUID regex pattern (v4) */
103
+ UUID: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
104
+ };
105
+ var ERRORS = {
106
+ /** Validation error messages */
107
+ VALIDATION: {
108
+ REQUIRED: (field) => `${field} is required`,
109
+ INVALID_TYPE: (field, type) => `${field} must be a ${type}`,
110
+ MIN_LENGTH: (field, min) => `${field} must be at least ${min} characters`,
111
+ MAX_LENGTH: (field, max) => `${field} must be at most ${max} characters`,
112
+ MIN_VALUE: (field, min) => `${field} must be at least ${min}`,
113
+ MAX_VALUE: (field, max) => `${field} must be at most ${max}`,
114
+ INVALID_FORMAT: (field) => `${field} format is invalid`,
115
+ INVALID_EMAIL: (field) => `${field} must be a valid email`,
116
+ INVALID_URL: (field) => `${field} must be a valid URL`,
117
+ INVALID_UUID: (field) => `${field} must be a valid UUID`,
118
+ INVALID_ENUM: (field, values) => `${field} must be one of: ${values.join(", ")}`,
119
+ MIN_ITEMS: (field, min) => `${field} must have at least ${min} items`,
120
+ MAX_ITEMS: (field, max) => `${field} must have at most ${max} items`
121
+ }
122
+ };
123
+
124
+ // src/core/errors.ts
125
+ var ArcisError = class extends Error {
126
+ constructor(message, statusCode = 500, code = "ARCIS_ERROR") {
127
+ super(message);
128
+ this.name = "ArcisError";
129
+ this.statusCode = statusCode;
130
+ this.code = code;
131
+ this.expose = statusCode < 500;
132
+ if (Error.captureStackTrace) {
133
+ Error.captureStackTrace(this, this.constructor);
134
+ }
135
+ }
136
+ };
137
+ var InputTooLargeError = class extends ArcisError {
138
+ constructor(maxSize, actualSize) {
139
+ super(`Input exceeds maximum size of ${maxSize} bytes`, 413, "INPUT_TOO_LARGE");
140
+ this.name = "InputTooLargeError";
141
+ this.maxSize = maxSize;
142
+ this.actualSize = actualSize;
143
+ }
144
+ };
145
+ var SecurityThreatError = class extends ArcisError {
146
+ constructor(threatType, pattern) {
147
+ super("Request blocked for security reasons", 400, "SECURITY_THREAT");
148
+ this.name = "SecurityThreatError";
149
+ this.threatType = threatType;
150
+ this.pattern = pattern;
151
+ }
152
+ };
153
+
154
+ // src/sanitizers/utils.ts
155
+ function encodeHtmlEntities(str) {
156
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
157
+ }
158
+
159
+ // src/sanitizers/xss.ts
160
+ function sanitizeXss(input, collectThreats = false, htmlEncode = false) {
161
+ if (typeof input !== "string") {
162
+ return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
163
+ }
164
+ const threats = [];
165
+ let value = input;
166
+ let wasSanitized = false;
167
+ for (const pattern of XSS_REMOVE_PATTERNS) {
168
+ pattern.lastIndex = 0;
169
+ if (pattern.test(value)) {
170
+ pattern.lastIndex = 0;
171
+ if (collectThreats) {
172
+ const matches = value.match(pattern);
173
+ if (matches) {
174
+ for (const match of matches) {
175
+ threats.push({
176
+ type: "xss",
177
+ pattern: pattern.source,
178
+ original: match
179
+ });
180
+ }
181
+ }
182
+ }
183
+ value = value.replace(pattern, "");
184
+ wasSanitized = true;
185
+ }
186
+ }
187
+ if (htmlEncode) {
188
+ const encoded = encodeHtmlEntities(value);
189
+ if (encoded !== value) {
190
+ wasSanitized = true;
191
+ }
192
+ value = encoded;
193
+ }
194
+ if (collectThreats) {
195
+ return { value, wasSanitized, threats };
196
+ }
197
+ return value;
198
+ }
199
+
200
+ // src/sanitizers/sql.ts
201
+ function sanitizeSql(input, collectThreats = false) {
202
+ if (typeof input !== "string") {
203
+ return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
204
+ }
205
+ const threats = [];
206
+ let value = input;
207
+ let wasSanitized = false;
208
+ for (const pattern of SQL_PATTERNS) {
209
+ pattern.lastIndex = 0;
210
+ if (pattern.test(value)) {
211
+ pattern.lastIndex = 0;
212
+ if (collectThreats) {
213
+ const matches = value.match(pattern);
214
+ if (matches) {
215
+ for (const match of matches) {
216
+ threats.push({
217
+ type: "sql_injection",
218
+ pattern: pattern.source,
219
+ original: match
220
+ });
221
+ }
222
+ }
223
+ }
224
+ value = value.replace(pattern, " ");
225
+ wasSanitized = true;
226
+ }
227
+ }
228
+ if (collectThreats) {
229
+ return { value, wasSanitized, threats };
230
+ }
231
+ return value;
232
+ }
233
+ function detectSql(input) {
234
+ if (typeof input !== "string") return false;
235
+ for (const pattern of SQL_PATTERNS) {
236
+ pattern.lastIndex = 0;
237
+ if (pattern.test(input)) {
238
+ return true;
239
+ }
240
+ }
241
+ return false;
242
+ }
243
+
244
+ // src/sanitizers/path.ts
245
+ function sanitizePath(input, collectThreats = false) {
246
+ if (typeof input !== "string") {
247
+ return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
248
+ }
249
+ const threats = [];
250
+ let value = input;
251
+ let wasSanitized = false;
252
+ for (const pattern of PATH_PATTERNS) {
253
+ pattern.lastIndex = 0;
254
+ if (pattern.test(value)) {
255
+ pattern.lastIndex = 0;
256
+ if (collectThreats) {
257
+ const matches = value.match(pattern);
258
+ if (matches) {
259
+ for (const match of matches) {
260
+ threats.push({
261
+ type: "path_traversal",
262
+ pattern: pattern.source,
263
+ original: match
264
+ });
265
+ }
266
+ }
267
+ }
268
+ value = value.replace(pattern, "");
269
+ wasSanitized = true;
270
+ }
271
+ }
272
+ if (collectThreats) {
273
+ return { value, wasSanitized, threats };
274
+ }
275
+ return value;
276
+ }
277
+
278
+ // src/sanitizers/command.ts
279
+ function sanitizeCommand(input, collectThreats = false) {
280
+ if (typeof input !== "string") {
281
+ return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
282
+ }
283
+ const threats = [];
284
+ let value = input;
285
+ let wasSanitized = false;
286
+ for (const pattern of COMMAND_PATTERNS) {
287
+ pattern.lastIndex = 0;
288
+ if (pattern.test(value)) {
289
+ pattern.lastIndex = 0;
290
+ if (collectThreats) {
291
+ const matches = value.match(pattern);
292
+ if (matches) {
293
+ for (const match of matches) {
294
+ threats.push({
295
+ type: "command_injection",
296
+ pattern: pattern.source,
297
+ original: match
298
+ });
299
+ }
300
+ }
301
+ }
302
+ value = value.replace(pattern, " ");
303
+ wasSanitized = true;
304
+ }
305
+ }
306
+ if (collectThreats) {
307
+ return { value, wasSanitized, threats };
308
+ }
309
+ return value;
310
+ }
311
+ function detectCommandInjection(input) {
312
+ if (typeof input !== "string") return false;
313
+ for (const pattern of COMMAND_PATTERNS) {
314
+ pattern.lastIndex = 0;
315
+ if (pattern.test(input)) {
316
+ return true;
317
+ }
318
+ }
319
+ return false;
320
+ }
321
+
322
+ // src/sanitizers/sanitize.ts
323
+ function sanitizeString(value, options = {}) {
324
+ if (typeof value !== "string") return value;
325
+ const maxSize = options.maxSize ?? INPUT.DEFAULT_MAX_SIZE;
326
+ if (value.length > maxSize) {
327
+ throw new InputTooLargeError(maxSize, value.length);
328
+ }
329
+ const reject = options.mode !== "sanitize";
330
+ let result = value;
331
+ if (options.sql !== false) {
332
+ if (reject) {
333
+ if (detectSql(result)) {
334
+ throw new SecurityThreatError("sql_injection", "SQL pattern detected in input");
335
+ }
336
+ } else {
337
+ result = sanitizeSql(result);
338
+ }
339
+ }
340
+ if (options.path !== false) {
341
+ result = sanitizePath(result);
342
+ }
343
+ if (options.command !== false) {
344
+ if (reject) {
345
+ if (detectCommandInjection(result)) {
346
+ throw new SecurityThreatError("command_injection", "Shell metacharacter detected in input");
347
+ }
348
+ } else {
349
+ result = sanitizeCommand(result);
350
+ }
351
+ }
352
+ if (options.xss !== false) {
353
+ result = sanitizeXss(result, false, options.htmlEncode ?? false);
354
+ }
355
+ return result;
356
+ }
357
+
358
+ // src/validation/schema.ts
359
+ function validate(schema, source = "body") {
360
+ return (req, res, next) => {
361
+ const data = req[source] || {};
362
+ const errors = [];
363
+ const validated = {};
364
+ for (const [field, rules] of Object.entries(schema)) {
365
+ const value = data[field];
366
+ const result = validateField(field, value, rules);
367
+ if (result.errors.length > 0) {
368
+ errors.push(...result.errors);
369
+ } else if (result.value !== void 0) {
370
+ validated[field] = result.value;
371
+ }
372
+ }
373
+ if (errors.length > 0) {
374
+ res.status(400).json({ errors });
375
+ return;
376
+ }
377
+ req[source] = validated;
378
+ next();
379
+ };
380
+ }
381
+ function validateField(field, value, rules) {
382
+ const errors = [];
383
+ if (rules.required && (value === void 0 || value === null || value === "")) {
384
+ errors.push(ERRORS.VALIDATION.REQUIRED(field));
385
+ return { errors };
386
+ }
387
+ if (value === void 0 || value === null) {
388
+ return { errors: [] };
389
+ }
390
+ let typedValue = value;
391
+ let isValid = true;
392
+ switch (rules.type) {
393
+ case "string":
394
+ if (typeof value !== "string") {
395
+ errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "string"));
396
+ isValid = false;
397
+ break;
398
+ }
399
+ if (rules.min !== void 0 && value.length < rules.min) {
400
+ errors.push(ERRORS.VALIDATION.MIN_LENGTH(field, rules.min));
401
+ isValid = false;
402
+ }
403
+ if (rules.max !== void 0 && value.length > rules.max) {
404
+ errors.push(ERRORS.VALIDATION.MAX_LENGTH(field, rules.max));
405
+ isValid = false;
406
+ }
407
+ if (rules.pattern && !rules.pattern.test(value)) {
408
+ errors.push(ERRORS.VALIDATION.INVALID_FORMAT(field));
409
+ isValid = false;
410
+ }
411
+ if (isValid && rules.enum && !rules.enum.includes(value)) {
412
+ errors.push(ERRORS.VALIDATION.INVALID_ENUM(field, rules.enum));
413
+ isValid = false;
414
+ }
415
+ if (isValid && rules.sanitize !== false) {
416
+ typedValue = sanitizeString(value);
417
+ }
418
+ break;
419
+ case "number":
420
+ typedValue = Number(value);
421
+ if (isNaN(typedValue)) {
422
+ errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "number"));
423
+ isValid = false;
424
+ break;
425
+ }
426
+ if (rules.min !== void 0 && typedValue < rules.min) {
427
+ errors.push(ERRORS.VALIDATION.MIN_VALUE(field, rules.min));
428
+ isValid = false;
429
+ }
430
+ if (rules.max !== void 0 && typedValue > rules.max) {
431
+ errors.push(ERRORS.VALIDATION.MAX_VALUE(field, rules.max));
432
+ isValid = false;
433
+ }
434
+ break;
435
+ case "boolean":
436
+ if (value === "true" || value === true || value === 1 || value === "1") {
437
+ typedValue = true;
438
+ } else if (value === "false" || value === false || value === 0 || value === "0") {
439
+ typedValue = false;
440
+ } else {
441
+ errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "boolean"));
442
+ isValid = false;
443
+ }
444
+ break;
445
+ case "email":
446
+ if (!VALIDATION.EMAIL.test(String(value))) {
447
+ errors.push(ERRORS.VALIDATION.INVALID_EMAIL(field));
448
+ isValid = false;
449
+ }
450
+ if (isValid) {
451
+ typedValue = sanitizeString(String(value).toLowerCase().trim());
452
+ }
453
+ break;
454
+ case "url":
455
+ if (!VALIDATION.URL.test(String(value))) {
456
+ errors.push(ERRORS.VALIDATION.INVALID_URL(field));
457
+ isValid = false;
458
+ }
459
+ if (isValid) {
460
+ typedValue = sanitizeString(String(value));
461
+ }
462
+ break;
463
+ case "uuid":
464
+ if (!VALIDATION.UUID.test(String(value))) {
465
+ errors.push(ERRORS.VALIDATION.INVALID_UUID(field));
466
+ isValid = false;
467
+ }
468
+ break;
469
+ case "array":
470
+ if (!Array.isArray(value)) {
471
+ errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "array"));
472
+ isValid = false;
473
+ break;
474
+ }
475
+ if (rules.min !== void 0 && value.length < rules.min) {
476
+ errors.push(ERRORS.VALIDATION.MIN_ITEMS(field, rules.min));
477
+ isValid = false;
478
+ }
479
+ if (rules.max !== void 0 && value.length > rules.max) {
480
+ errors.push(ERRORS.VALIDATION.MAX_ITEMS(field, rules.max));
481
+ isValid = false;
482
+ }
483
+ break;
484
+ case "object":
485
+ if (typeof value !== "object" || Array.isArray(value) || value === null) {
486
+ errors.push(ERRORS.VALIDATION.INVALID_TYPE(field, "object"));
487
+ isValid = false;
488
+ }
489
+ break;
490
+ }
491
+ if (isValid && rules.enum && rules.type !== "string" && !rules.enum.includes(typedValue)) {
492
+ errors.push(ERRORS.VALIDATION.INVALID_ENUM(field, rules.enum));
493
+ isValid = false;
494
+ }
495
+ if (isValid && rules.custom) {
496
+ const customResult = rules.custom(typedValue);
497
+ if (customResult === void 0) {
498
+ throw new TypeError(
499
+ `Custom validator for field "${field}" returned undefined. Return true to pass, false to fail, or a string error message.`
500
+ );
501
+ }
502
+ if (customResult !== true) {
503
+ errors.push(typeof customResult === "string" && customResult.length > 0 ? customResult : `${field} is invalid`);
504
+ isValid = false;
505
+ }
506
+ }
507
+ return {
508
+ value: isValid ? typedValue : void 0,
509
+ errors
510
+ };
511
+ }
512
+ var createValidator = validate;
513
+
514
+ // src/validation/file.ts
515
+ var MAGIC_BYTES = {
516
+ // Images
517
+ "image/jpeg": [Buffer.from([255, 216, 255])],
518
+ "image/png": [Buffer.from([137, 80, 78, 71])],
519
+ "image/gif": [Buffer.from("GIF87a"), Buffer.from("GIF89a")],
520
+ "image/webp": [Buffer.from("RIFF")],
521
+ // RIFF....WEBP
522
+ "image/bmp": [Buffer.from([66, 77])],
523
+ "image/svg+xml": [],
524
+ // text-based, check separately
525
+ // Documents
526
+ "application/pdf": [Buffer.from("%PDF")],
527
+ "application/zip": [Buffer.from([80, 75, 3, 4])],
528
+ // Audio/Video
529
+ "audio/mpeg": [Buffer.from([255, 251]), Buffer.from([255, 243]), Buffer.from([73, 68, 51])],
530
+ "video/mp4": []
531
+ // ftyp at offset 4
532
+ };
533
+ var DANGEROUS_EXTENSIONS = /* @__PURE__ */ new Set([
534
+ // Scripts
535
+ ".exe",
536
+ ".bat",
537
+ ".cmd",
538
+ ".com",
539
+ ".msi",
540
+ ".scr",
541
+ ".pif",
542
+ ".vbs",
543
+ ".vbe",
544
+ ".js",
545
+ ".jse",
546
+ ".ws",
547
+ ".wsf",
548
+ ".wsc",
549
+ ".wsh",
550
+ ".ps1",
551
+ ".ps1xml",
552
+ ".ps2",
553
+ ".ps2xml",
554
+ ".psc1",
555
+ ".psc2",
556
+ ".sh",
557
+ ".bash",
558
+ ".csh",
559
+ ".ksh",
560
+ // Server-side
561
+ ".php",
562
+ ".php3",
563
+ ".php4",
564
+ ".php5",
565
+ ".phtml",
566
+ ".pht",
567
+ ".asp",
568
+ ".aspx",
569
+ ".ashx",
570
+ ".asmx",
571
+ ".cer",
572
+ ".jsp",
573
+ ".jspx",
574
+ ".jsw",
575
+ ".jsv",
576
+ ".cgi",
577
+ ".pl",
578
+ ".py",
579
+ ".rb",
580
+ // Java
581
+ ".jar",
582
+ ".war",
583
+ ".ear",
584
+ ".class",
585
+ // Config that can execute
586
+ ".htaccess",
587
+ ".htpasswd",
588
+ // Template engines
589
+ ".ejs",
590
+ ".pug",
591
+ ".hbs",
592
+ ".handlebars",
593
+ ".njk",
594
+ ".twig",
595
+ // Shortcuts/links
596
+ ".lnk",
597
+ ".inf",
598
+ ".reg",
599
+ ".url",
600
+ // Office macros
601
+ ".docm",
602
+ ".xlsm",
603
+ ".pptm",
604
+ ".dotm"
605
+ ]);
606
+ var DEFAULT_MAX_SIZE = 5 * 1024 * 1024;
607
+ function sanitizeFilename(filename) {
608
+ let name = filename;
609
+ name = name.replace(/\0/g, "");
610
+ name = name.replace(/^.*[/\\]/, "");
611
+ name = name.replace(/[\x00-\x1F\x7F]/g, "");
612
+ name = name.replace(/[<>:"/\\|?*]/g, "");
613
+ name = name.replace(/[\s()]+/g, "_");
614
+ name = name.replace(/^\.+/, "");
615
+ name = name.replace(/_{2,}/g, "_");
616
+ name = name.replace(/\.{2,}/g, ".");
617
+ name = name.replace(/_+\./g, ".");
618
+ name = name.replace(/^_+|_+$/g, "");
619
+ if (!name || name === ".") {
620
+ name = "unnamed";
621
+ }
622
+ return name;
623
+ }
624
+ function matchesMagicBytes(buffer, mimetype) {
625
+ const signatures = MAGIC_BYTES[mimetype];
626
+ if (!signatures || signatures.length === 0) return true;
627
+ return signatures.some((sig) => {
628
+ if (buffer.length < sig.length) return false;
629
+ return buffer.subarray(0, sig.length).equals(sig);
630
+ });
631
+ }
632
+ function getExtension(filename) {
633
+ const lastDot = filename.lastIndexOf(".");
634
+ if (lastDot < 1) return "";
635
+ return filename.slice(lastDot).toLowerCase();
636
+ }
637
+ function hasDoubleExtension(filename) {
638
+ const parts = filename.split(".");
639
+ if (parts.length < 3) return false;
640
+ for (let i = 1; i < parts.length - 1; i++) {
641
+ const ext = "." + parts[i].toLowerCase();
642
+ if (DANGEROUS_EXTENSIONS.has(ext)) return true;
643
+ }
644
+ return false;
645
+ }
646
+ function validateFile(file, options = {}) {
647
+ const {
648
+ maxSize = DEFAULT_MAX_SIZE,
649
+ allowedTypes,
650
+ allowedExtensions,
651
+ blockExecutables = true,
652
+ validateMagicBytes = true,
653
+ blockNoExtension = true,
654
+ blockDoubleExtensions = true
655
+ } = options;
656
+ const errors = [];
657
+ const sanitizedFilename = sanitizeFilename(file.filename);
658
+ const extension = getExtension(sanitizedFilename);
659
+ if (file.size > maxSize) {
660
+ errors.push(`File size ${file.size} exceeds maximum ${maxSize} bytes`);
661
+ }
662
+ if (file.size === 0) {
663
+ errors.push("File is empty");
664
+ }
665
+ if (blockNoExtension && !extension) {
666
+ errors.push("File has no extension");
667
+ }
668
+ if (blockExecutables && extension && DANGEROUS_EXTENSIONS.has(extension)) {
669
+ errors.push(`Executable extension "${extension}" is not allowed`);
670
+ }
671
+ if (blockDoubleExtensions && hasDoubleExtension(sanitizedFilename)) {
672
+ errors.push("Double extensions with executable types are not allowed");
673
+ }
674
+ if (allowedExtensions && extension) {
675
+ const normalizedAllowed = allowedExtensions.map((e) => e.toLowerCase());
676
+ if (!normalizedAllowed.includes(extension)) {
677
+ errors.push(`Extension "${extension}" is not allowed. Allowed: ${normalizedAllowed.join(", ")}`);
678
+ }
679
+ }
680
+ if (allowedTypes && !allowedTypes.includes(file.mimetype)) {
681
+ errors.push(`MIME type "${file.mimetype}" is not allowed. Allowed: ${allowedTypes.join(", ")}`);
682
+ }
683
+ if (validateMagicBytes && file.buffer && file.buffer.length > 0) {
684
+ if (!matchesMagicBytes(file.buffer, file.mimetype)) {
685
+ errors.push(`File content does not match claimed MIME type "${file.mimetype}"`);
686
+ }
687
+ }
688
+ return {
689
+ valid: errors.length === 0,
690
+ errors,
691
+ sanitizedFilename
692
+ };
693
+ }
694
+ function isDangerousExtension(filename) {
695
+ const ext = getExtension(filename);
696
+ return ext !== "" && DANGEROUS_EXTENSIONS.has(ext);
697
+ }
698
+
699
+ exports.createValidator = createValidator;
700
+ exports.isDangerousExtension = isDangerousExtension;
701
+ exports.sanitizeFilename = sanitizeFilename;
702
+ exports.validate = validate;
703
+ exports.validateFile = validateFile;
704
+ //# sourceMappingURL=index.js.map
705
+ //# sourceMappingURL=index.js.map