{"_id":"@competentgroove/secure-upload","_rev":"5-14094519b0e918eeacfeb2926681be96","name":"@competentgroove/secure-upload","dist-tags":{"latest":"1.0.4"},"versions":{"1.0.0":{"name":"@competentgroove/secure-upload","version":"1.0.0","keywords":["file-upload","security","validation","mime","sanitization","magic-bytes","zip-bomb","pdf-security","svg-xss","formula-injection"],"author":{"name":"competentgroove"},"license":"MIT","_id":"@competentgroove/secure-upload@1.0.0","maintainers":[{"name":"deepaksharma_groove","email":"tech@competentgroove.com"},{"name":"nitinrajput","email":"nitin@competentgroove.com"}],"homepage":"https://github.com/competentgroove/secure-upload#readme","bugs":{"url":"https://github.com/competentgroove/secure-upload/issues"},"dist":{"shasum":"9ff04f7a910da7b637e2044d0995f2b219c94662","tarball":"https://registry.npmjs.org/@competentgroove/secure-upload/-/secure-upload-1.0.0.tgz","fileCount":46,"integrity":"sha512-DAo4pEApcKTCsvwt05mJkIfP9C2/4mmK4znsmNl9VjgMQkkXlmFANKXCODsr+lar+gwRgs0JvX/LkqEO+QbC7Q==","signatures":[{"sig":"MEUCIQD56K0o5hqScoDzU7biEDSHaNjTGC6QGKfyH3QVMitKBQIgJLTBB+vZbiDk0Si9vDUq7HsDU5q/JhkiiDyioTb/Oms=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":460979},"main":"./dist/index.cjs","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20.12.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./functions":{"types":"./dist/functions.d.ts","import":"./dist/functions.js","require":"./dist/functions.cjs"},"./middleware/koa":{"types":"./dist/middleware/koa.d.ts","import":"./dist/middleware/koa.js","require":"./dist/middleware/koa.cjs"},"./middleware/express":{"types":"./dist/middleware/express.d.ts","import":"./dist/middleware/express.js","require":"./dist/middleware/express.cjs"},"./middleware/fastify":{"types":"./dist/middleware/fastify.d.ts","import":"./dist/middleware/fastify.js","require":"./dist/middleware/fastify.cjs"}},"gitHead":"a763990ef200cafa751b0d1f625f68ccf1c6535a","scripts":{"dev":"tsup --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsup","benchmark":"tsx tests/performance/benchmark.ts","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run typecheck && npm run build && npm test"},"_npmUser":{"name":"nitinrajput","email":"nitin@competentgroove.com"},"repository":{"url":"git+https://github.com/competentgroove/secure-upload.git","type":"git"},"_npmVersion":"10.9.4","description":"Production-ready secure file upload validation and sanitization using magic-byte detection","directories":{},"sideEffects":false,"_nodeVersion":"22.21.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.7.0","tsup":"^8.0.2","vitest":"^4.1.6","typescript":"^5.4.0","@types/node":"^20.11.0","@vitest/coverage-v8":"^4.1.6"},"peerDependencies":{"koa":">=2.0.0","express":">=4.0.0","fastify":">=4.0.0"},"peerDependenciesMeta":{"koa":{"optional":true},"express":{"optional":true},"fastify":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/secure-upload_1.0.0_1778754135547_0.9556629233444571","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@competentgroove/secure-upload","version":"1.0.1","keywords":["file-upload","security","validation","mime","sanitization","magic-bytes","zip-bomb","pdf-security","svg-xss","formula-injection"],"author":{"name":"competentgroove"},"license":"MIT","_id":"@competentgroove/secure-upload@1.0.1","maintainers":[{"name":"deepaksharma_groove","email":"tech@competentgroove.com"},{"name":"nitinrajput","email":"nitin@competentgroove.com"}],"homepage":"https://github.com/competentgroove/secure-upload#readme","bugs":{"url":"https://github.com/competentgroove/secure-upload/issues"},"dist":{"shasum":"9cbafb4dcd7e1e487e0d890b8336c736d930dbfb","tarball":"https://registry.npmjs.org/@competentgroove/secure-upload/-/secure-upload-1.0.1.tgz","fileCount":46,"integrity":"sha512-yzAiJsVes2vUk8p1yV+r56tvz5b5vwwbRDb7x7adMtTFG1/rs/tEQ1a3Guu3qGhYgUw32mkNZPpkJKkv9hi7hw==","signatures":[{"sig":"MEYCIQCUybGiZwl1Q+T+SgwncGVw44Og8EW3lraZkhcLIt5pWgIhAKPiIpozQUOuIwA1OzfAuWTWx4LBpVRP2u+h3Yi92UFK","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":461071},"main":"./dist/index.cjs","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20.12.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./functions":{"types":"./dist/functions.d.ts","import":"./dist/functions.js","require":"./dist/functions.cjs"},"./middleware/koa":{"types":"./dist/middleware/koa.d.ts","import":"./dist/middleware/koa.js","require":"./dist/middleware/koa.cjs"},"./middleware/express":{"types":"./dist/middleware/express.d.ts","import":"./dist/middleware/express.js","require":"./dist/middleware/express.cjs"},"./middleware/fastify":{"types":"./dist/middleware/fastify.d.ts","import":"./dist/middleware/fastify.js","require":"./dist/middleware/fastify.cjs"}},"gitHead":"36077d4e60a559b3571e7ec69194f680ec0ba8e1","scripts":{"dev":"tsup --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsup","benchmark":"tsx tests/performance/benchmark.ts","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run typecheck && npm run build && npm test"},"_npmUser":{"name":"nitinrajput","email":"nitin@competentgroove.com"},"repository":{"url":"git+https://github.com/competentgroove/secure-upload.git","type":"git"},"_npmVersion":"10.9.4","description":"Production-ready secure file upload validation and sanitization using magic-byte detection","directories":{},"sideEffects":false,"_nodeVersion":"22.21.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.7.0","tsup":"^8.0.2","vitest":"^4.1.6","typescript":"^5.4.0","@types/node":"^20.11.0","@vitest/coverage-v8":"^4.1.6"},"peerDependencies":{"koa":">=2.0.0","express":">=4.0.0","fastify":">=4.0.0"},"peerDependenciesMeta":{"koa":{"optional":true},"express":{"optional":true},"fastify":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/secure-upload_1.0.1_1778757824370_0.17740265863721505","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@competentgroove/secure-upload","version":"1.0.2","keywords":["file-upload","security","validation","mime","sanitization","magic-bytes","zip-bomb","pdf-security","svg-xss","formula-injection"],"author":{"name":"competentgroove"},"license":"MIT","_id":"@competentgroove/secure-upload@1.0.2","maintainers":[{"name":"deepaksharma_groove","email":"tech@competentgroove.com"},{"name":"nitinrajput","email":"nitin@competentgroove.com"}],"homepage":"https://github.com/competentgroove/secure-upload#readme","bugs":{"url":"https://github.com/competentgroove/secure-upload/issues"},"dist":{"shasum":"884ae10544f4ba358ac9d3279d7964daa4c7f75b","tarball":"https://registry.npmjs.org/@competentgroove/secure-upload/-/secure-upload-1.0.2.tgz","fileCount":46,"integrity":"sha512-iP+vCcBAAUAxbq2u5Gylx0JU/1vv+FD8ff/T+zVSCCNksSTz/nJBiA1AFwl6ic4wvXbk9/oHBn01+GFZuekGuQ==","signatures":[{"sig":"MEYCIQD0XmhZ2VzMvD3jtNlfq4m4hFqx+/me4tDJoLYZiCvj/QIhAJyFE+JQ/jtVPfTbu/lDQFk8+VCb3/8F2Sub5cyyQ643","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":478275},"main":"./dist/index.cjs","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20.12.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./functions":{"types":"./dist/functions.d.ts","import":"./dist/functions.js","require":"./dist/functions.cjs"},"./middleware/koa":{"types":"./dist/middleware/koa.d.ts","import":"./dist/middleware/koa.js","require":"./dist/middleware/koa.cjs"},"./middleware/express":{"types":"./dist/middleware/express.d.ts","import":"./dist/middleware/express.js","require":"./dist/middleware/express.cjs"},"./middleware/fastify":{"types":"./dist/middleware/fastify.d.ts","import":"./dist/middleware/fastify.js","require":"./dist/middleware/fastify.cjs"}},"gitHead":"8ed89c0bc81f8a2a4857395de479704b65eaea6c","scripts":{"dev":"tsup --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsup","benchmark":"tsx tests/performance/benchmark.ts","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run typecheck && npm run build && npm test"},"_npmUser":{"name":"nitinrajput","email":"nitin@competentgroove.com"},"repository":{"url":"git+https://github.com/competentgroove/secure-upload.git","type":"git"},"_npmVersion":"10.9.4","description":"Production-ready secure file upload validation and sanitization using magic-byte detection","directories":{},"sideEffects":false,"_nodeVersion":"22.21.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.7.0","tsup":"^8.0.2","vitest":"^4.1.6","typescript":"^5.4.0","@types/node":"^20.11.0","@vitest/coverage-v8":"^4.1.6"},"peerDependencies":{"koa":">=2.0.0","express":">=4.0.0","fastify":">=4.0.0"},"peerDependenciesMeta":{"koa":{"optional":true},"express":{"optional":true},"fastify":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/secure-upload_1.0.2_1779100873862_0.841389033044442","host":"s3://npm-registry-packages-npm-production"}},"1.0.3":{"name":"@competentgroove/secure-upload","version":"1.0.3","keywords":["file-upload","security","validation","mime","sanitization","magic-bytes","zip-bomb","pdf-security","svg-xss","formula-injection"],"author":{"name":"competentgroove"},"license":"MIT","_id":"@competentgroove/secure-upload@1.0.3","maintainers":[{"name":"deepaksharma_groove","email":"tech@competentgroove.com"},{"name":"nitinrajput","email":"nitin@competentgroove.com"}],"homepage":"https://github.com/competentgroove/secure-upload#readme","bugs":{"url":"https://github.com/competentgroove/secure-upload/issues"},"dist":{"shasum":"ab8b933ea0fa8f10fa1af3ff66196ebf5e26fac2","tarball":"https://registry.npmjs.org/@competentgroove/secure-upload/-/secure-upload-1.0.3.tgz","fileCount":46,"integrity":"sha512-8kJfaECWJ9tZNzI/AaUpDodm8EpUUh7aUoaWl1leNeUl9uvC8G8JxNa/SVfsAo/u9IV6fPeiuOG9c4vZlhMQ+g==","signatures":[{"sig":"MEUCIAgfA7qxd/dCSJ5wwTPh20l/SOmdTL6N+GviogQeJkZvAiEAynTUwisvdFOwiDVpJ9Bp9gXiXOHwd2cLL18ASh5pfyA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":483625},"main":"./dist/index.cjs","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20.12.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./functions":{"types":"./dist/functions.d.ts","import":"./dist/functions.js","require":"./dist/functions.cjs"},"./middleware/koa":{"types":"./dist/middleware/koa.d.ts","import":"./dist/middleware/koa.js","require":"./dist/middleware/koa.cjs"},"./middleware/express":{"types":"./dist/middleware/express.d.ts","import":"./dist/middleware/express.js","require":"./dist/middleware/express.cjs"},"./middleware/fastify":{"types":"./dist/middleware/fastify.d.ts","import":"./dist/middleware/fastify.js","require":"./dist/middleware/fastify.cjs"}},"gitHead":"cb1b62e02ef9a2fd1643f66f4f9374a249bf3fd6","scripts":{"dev":"tsup --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsup","benchmark":"tsx tests/performance/benchmark.ts","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run typecheck && npm run build && npm test"},"_npmUser":{"name":"nitinrajput","email":"nitin@competentgroove.com"},"repository":{"url":"git+https://github.com/competentgroove/secure-upload.git","type":"git"},"_npmVersion":"10.9.4","description":"Production-ready secure file upload validation and sanitization using magic-byte detection","directories":{},"sideEffects":false,"_nodeVersion":"22.21.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.7.0","tsup":"^8.0.2","vitest":"^4.1.6","typescript":"^5.4.0","@types/node":"^20.11.0","@vitest/coverage-v8":"^4.1.6"},"peerDependencies":{"koa":">=2.0.0","express":">=4.0.0","fastify":">=4.0.0"},"peerDependenciesMeta":{"koa":{"optional":true},"express":{"optional":true},"fastify":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/secure-upload_1.0.3_1779383060745_0.46906853179613606","host":"s3://npm-registry-packages-npm-production"}},"1.0.4":{"name":"@competentgroove/secure-upload","version":"1.0.4","description":"Production-ready secure file upload validation and sanitization using magic-byte detection","keywords":["file-upload","security","validation","mime","sanitization","magic-bytes","zip-bomb","pdf-security","svg-xss","formula-injection"],"author":{"name":"competentgroove"},"publishConfig":{"access":"public"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/competentgroove/secure-upload.git"},"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./functions":{"types":"./dist/functions.d.ts","import":"./dist/functions.js","require":"./dist/functions.cjs"},"./middleware/express":{"types":"./dist/middleware/express.d.ts","import":"./dist/middleware/express.js","require":"./dist/middleware/express.cjs"},"./middleware/fastify":{"types":"./dist/middleware/fastify.d.ts","import":"./dist/middleware/fastify.js","require":"./dist/middleware/fastify.cjs"},"./middleware/koa":{"types":"./dist/middleware/koa.d.ts","import":"./dist/middleware/koa.js","require":"./dist/middleware/koa.cjs"}},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","benchmark":"tsx tests/performance/benchmark.ts","typecheck":"tsc --noEmit","lint":"tsc --noEmit","prepublishOnly":"npm run typecheck && npm run build && npm test"},"devDependencies":{"@types/node":"^20.11.0","@vitest/coverage-v8":"^4.1.6","tsup":"^8.0.2","tsx":"^4.7.0","typescript":"^5.4.0","vitest":"^4.1.6"},"peerDependencies":{"express":">=4.0.0","fastify":">=4.0.0","koa":">=2.0.0"},"peerDependenciesMeta":{"express":{"optional":true},"fastify":{"optional":true},"koa":{"optional":true}},"engines":{"node":">=20.12.0"},"sideEffects":false,"_id":"@competentgroove/secure-upload@1.0.4","gitHead":"b28c70929c1380790b4d35f88fd9ccdc6d51dde3","bugs":{"url":"https://github.com/competentgroove/secure-upload/issues"},"homepage":"https://github.com/competentgroove/secure-upload#readme","_nodeVersion":"22.21.1","_npmVersion":"10.9.4","dist":{"integrity":"sha512-6Zw/t7A1wvxBrFcawP/afftmOK+9H4rM/gZI2shD56LO078SIAmgYCSLrKAYlI87I/vCiwUOS/RCxmf/pzMgZA==","shasum":"8db2b6581fda0a9761b8be1e7ab81619b5807c83","tarball":"https://registry.npmjs.org/@competentgroove/secure-upload/-/secure-upload-1.0.4.tgz","fileCount":46,"unpackedSize":485705,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBI28mHbDDzbnCOMYmDrMiP4GdWKqSG3syqxNi3WrBmIAiBm210ZpB3p2JldZjfcdBEXIGYaNVzpIAe5Y/ValMeFfg=="}]},"_npmUser":{"name":"nitinrajput","email":"nitin@competentgroove.com"},"directories":{},"maintainers":[{"name":"deepaksharma_groove","email":"tech@competentgroove.com"},{"name":"nitinrajput","email":"nitin@competentgroove.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/secure-upload_1.0.4_1779383990541_0.8598163382364434"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-14T10:22:15.421Z","modified":"2026-05-21T17:19:50.831Z","1.0.0":"2026-05-14T10:22:15.779Z","1.0.1":"2026-05-14T11:23:44.569Z","1.0.2":"2026-05-18T10:41:14.024Z","1.0.3":"2026-05-21T17:04:20.867Z","1.0.4":"2026-05-21T17:19:50.702Z"},"bugs":{"url":"https://github.com/competentgroove/secure-upload/issues"},"author":{"name":"competentgroove"},"license":"MIT","homepage":"https://github.com/competentgroove/secure-upload#readme","keywords":["file-upload","security","validation","mime","sanitization","magic-bytes","zip-bomb","pdf-security","svg-xss","formula-injection"],"repository":{"type":"git","url":"git+https://github.com/competentgroove/secure-upload.git"},"description":"Production-ready secure file upload validation and sanitization using magic-byte detection","maintainers":[{"name":"deepaksharma_groove","email":"tech@competentgroove.com"},{"name":"nitinrajput","email":"nitin@competentgroove.com"}],"readme":"# secure-upload\n\n> Production-ready secure file upload validation and sanitization for Node.js.\n\n[![CI](https://github.com/competentgroove/fileguard/actions/workflows/ci.yml/badge.svg)](https://github.com/competentgroove/fileguard/actions)\n[![npm](https://img.shields.io/npm/v/@competentgroove/fileguard)](https://www.npmjs.com/package/@competentgroove/fileguard)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue)](tsconfig.json)\n\n**secure-upload** uses **magic-byte detection** to determine the real type of uploaded files — not file extensions, not Content-Type headers. It then applies type-specific security rules to catch dangerous payloads before they reach your storage or processing pipeline.\n\n---\n\n## Table of Contents\n\n- [Why secure-upload?](#why-secure-upload)\n- [Threat Coverage](#threat-coverage)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [API Reference](#api-reference)\n- [Configuration Reference](#configuration-reference)\n- [Mobile Upload Behaviour](#mobile-upload-behaviour)\n- [Framework Integrations](#framework-integrations)\n- [Validation Result Format](#validation-result-format)\n- [Issue Codes](#issue-codes)\n- [Validation Pipeline](#validation-pipeline)\n- [Plugin System](#plugin-system)\n- [Security Recommendations](#security-recommendations)\n- [Roadmap](#roadmap)\n\n---\n\n## Why secure-upload?\n\nMost file validation libraries rely on MIME type headers or file extensions. Both are trivially spoofable by attackers:\n\n```\n# Extension spoofed: malware.exe → document.pdf\n# MIME spoofed: Content-Type: image/png (but file is actually a PHP shell)\n```\n\n**secure-upload** reads the first bytes of the file (the *magic bytes*) to determine the true format, then applies type-specific security rules. Spoofing magic bytes in a way that also fools parsers is much harder.\n\n---\n\n## Threat Coverage\n\n| Threat | Detection |\n|--------|-----------|\n| MIME spoofing | Magic-byte detection vs. client MIME |\n| Extension spoofing | Detected ext vs. filename ext |\n| PDF with JavaScript | `/JavaScript`, `/JS` keyword scan |\n| PDF with auto-execute | `/OpenAction`, `/Launch` scan |\n| SVG XSS | `<script>`, `on*=`, `foreignObject` |\n| SVG SSRF | External `href` references |\n| CSV formula injection | Leading `=`, `+`, `-`, `@` detection |\n| ZIP bomb | Entry count, uncompressed size, ratio |\n| ZIP path traversal | `../` in entry names |\n| Nested archives | Configurable reject |\n| Office macros | `vbaProject.bin` detection |\n| JSON prototype pollution | `__proto__`, `constructor` key detection |\n| Malformed images | JPEG/PNG/GIF/WebP header validation |\n| Audio/video type spoofing | Magic-byte detection for M4A, MP4, MOV, 3GP, WebM, MP3, WAV, AAC |\n\n---\n\n## Installation\n\n```bash\nnpm install @competentgroove/secure-upload\n```\n\n**No required runtime dependencies.** Framework middleware is optional and uses peer dependencies.\n\n```bash\n# For Express middleware\nnpm install express multer\n\n# For Fastify plugin\nnpm install fastify @fastify/multipart\n\n# For Koa middleware\nnpm install koa koa-body\n```\n\n**Node.js 20.12+ required.**\n\n---\n\n## Usage Styles\n\nsecure-upload works in **CommonJS**, **ESM**, and **TypeScript** — pick whichever fits your stack.\n\n### CommonJS (plain `.js`, no TypeScript needed)\n\n```js\n// Import only what you need — no class instantiation required\nconst { validatePdf, validateImage, validateSvg, validateCsv, validateJson, validateAny } = require('@competentgroove/secure-upload');\n\n// Validate a PDF\nconst result = await validatePdf(fs.readFileSync('upload.pdf'), {\n  allowJavaScript: false,\n  rejectEncrypted: true,\n});\nif (!result.valid) console.error(result.detectedIssues);\n\n// Validate + sanitize SVG in one call\nconst svgResult = await validateSvg(buffer, { allowScripts: false, sanitize: true });\nconst safeContent = svgResult.sanitized ?? buffer;  // sanitized when threats were removed\n\n// Auto-detect type and validate with a single call\nconst anyResult = await validateAny(buffer, {\n  allowedMimeTypes: ['application/pdf', 'image/png', 'image/jpeg'],\n  pdf: { allowJavaScript: false },\n  svg: { allowScripts: false },\n});\n```\n\n### ESM / TypeScript\n\n```typescript\nimport { validatePdf, validateImage, validateAny } from '@competentgroove/secure-upload';\n// same API, full type safety\n```\n\n### Class-based (for shared config across many requests)\n\n```typescript\nimport { createValidator } from '@competentgroove/secure-upload';\n\nconst validator = createValidator({\n  allowedMimeTypes: ['application/pdf', 'image/png'],\n  pdf: { allowJavaScript: false },\n});\n\nconst result = await validator.validateBuffer(buffer);\n```\n\n---\n\n## Quick Start\n\n```typescript\nimport { createValidator } from '@competentgroove/secure-upload';\n\nconst validator = createValidator({\n  allowedMimeTypes: [\n    'application/pdf',\n    'image/png',\n    'image/jpeg',\n    'image/webp',\n  ],\n\n  maxFileSize: 10 * 1024 * 1024, // 10 MB\n\n  pdf: {\n    allowJavaScript: false,      // reject /JavaScript\n    allowOpenAction: false,      // reject auto-execute on open\n    allowLaunchAction: false,    // reject program launch\n    allowEmbeddedFiles: false,   // reject embedded file attachments\n    rejectEncrypted: true,       // reject encrypted PDFs\n  },\n\n  svg: {\n    allowScripts: false,\n    allowEventHandlers: false,\n    sanitize: true,              // return cleaned buffer in result.sanitized\n  },\n\n  csv: {\n    preventFormulaInjection: true,\n    escapeFormulas: true,        // return escaped buffer in result.sanitized\n  },\n\n  zip: {\n    maxEntries: 100,\n    maxUncompressedSize: 200 * 1024 * 1024, // 200 MB\n    maxCompressionRatio: 100,\n    rejectNestedArchives: true,\n  },\n\n  json: {\n    maxDepth: 30,\n    maxKeys: 5000,\n    detectPrototypePollution: true,\n  },\n});\n\n// Validate a Buffer\nconst result = await validator.validateBuffer({\n  buffer: fileBuffer,\n  filename: 'upload.pdf',      // used for extension hint\n  mimeType: 'application/pdf', // client-supplied (compared, not trusted)\n});\n\nif (!result.valid) {\n  console.error('Rejected:', result.detectedIssues);\n} else {\n  console.log('Accepted:', result.mime, result.ext);\n  // If sanitization was enabled, use result.sanitized\n  const safeContent = result.sanitized ?? fileBuffer;\n}\n```\n\n---\n\n## API Reference\n\n### Standalone functions\n\nAll functions accept either a plain `Buffer` or `{ buffer, filename?, mimeType? }`.\n\n| Function | Config type | Auto-sanitizes |\n|----------|-------------|----------------|\n| `validatePdf(input, config?)` | `PdfConfig` | No |\n| `validateImage(input, config?)` | `ImageConfig` | No |\n| `validateSvg(input, config?)` | `SvgConfig` | Yes — when `sanitize: true` |\n| `validateCsv(input, config?)` | `CsvConfig` | Yes — when `escapeFormulas: true` |\n| `validateTxt(input, config?)` | `ValidatorConfig` | No |\n| `validateZip(input, config?)` | `ZipConfig` | No |\n| `validateOffice(input, config?)` | `OfficeConfig` | No |\n| `validateJson(input, config?)` | `JsonConfig` | No |\n| `validateAny(input, config?)` | `ValidatorConfig` | Format-dependent |\n\n```js\n// CommonJS\nconst { validatePdf, validateSvg, validateCsv, validateJson, validateAny } = require('@competentgroove/secure-upload');\n\n// Also available from dedicated subpath (tree-shakeable)\nconst { validatePdf } = require('@competentgroove/secure-upload/functions');\n```\n\n```ts\n// TypeScript / ESM\nimport { validatePdf, validateSvg, validateAny } from '@competentgroove/secure-upload';\n```\n\n#### Sanitization\n\nWhen a sanitizer is available and active (e.g. SVG with `sanitize: true`, CSV with `escapeFormulas: true`), the returned `ValidationResult` includes a `sanitized` buffer:\n\n```js\nconst result = await validateSvg(buffer, { allowScripts: false, sanitize: true });\n// Issues still listed in result.detectedIssues for audit trail\n// Safe content available in result.sanitized (undefined if nothing was changed)\nconst safeBuffer = result.sanitized ?? buffer;\n```\n\n---\n\n### `createValidator(config?, extraValidators?)`\n\nReturns a `FileGuard` instance.\n\n```typescript\nimport { createValidator } from '@competentgroove/secure-upload';\nconst guard = createValidator(config, extraValidators);\n```\n\n### `guard.validateBuffer(input)`\n\n```typescript\n// Accepts a raw Buffer or a BufferInput object\nconst result = await guard.validateBuffer(buffer);\nconst result = await guard.validateBuffer({\n  buffer,\n  filename: 'photo.jpg',       // optional — used for extension check\n  mimeType: 'image/jpeg',      // optional — compared against detected type\n});\n```\n\n### `guard.validateFile(filePath, options?)`\n\nReads the file from disk, enforcing the `maxFileSize` limit without loading the entire file if it exceeds the limit.\n\n```typescript\nconst result = await guard.validateFile('/tmp/upload/abc123', {\n  filename: 'original-name.pdf',\n  mimeType: 'application/pdf',\n});\n```\n\n### `guard.validateStream(input)`\n\nCollects the stream into memory up to `maxFileSize` then validates.\n\n```typescript\n// Accepts a Readable stream or a StreamInput object\nconst result = await guard.validateStream(readableStream);\nconst result = await guard.validateStream({\n  stream: readableStream,\n  filename: 'upload.png',\n  mimeType: 'image/png',\n});\n```\n\n---\n\n## Configuration Reference\n\nAll options are optional. Defaults are shown.\n\n```typescript\ninterface ValidatorConfig {\n  // Allowed MIME types (detected, not client-supplied). Empty = allow all.\n  allowedMimeTypes?: string[];\n\n  // Maximum file size in bytes. Default: 52,428,800 (50 MB)\n  maxFileSize?: number;\n\n  // Fail when detected MIME ≠ client-supplied MIME. Default: true\n  strictMimeCheck?: boolean;\n\n  // Fail when detected extension ≠ filename extension. Default: false\n  strictExtensionCheck?: boolean;\n\n  // Exempt specific detected/client MIME pairs from strict checks. Default: []\n  // Useful for mobile uploads where the OS reports the wrong MIME type.\n  mimeExceptions?: Array<{ detected: string; clientMime: string }>;\n\n  // Enable debug logging. Default: false\n  debug?: boolean;\n\n  pdf?: {\n    allowJavaScript?: boolean;      // default: false\n    allowOpenAction?: boolean;      // default: false\n    allowLaunchAction?: boolean;    // default: false\n    allowEmbeddedFiles?: boolean;   // default: false\n    allowXFA?: boolean;             // default: false\n    allowRichMedia?: boolean;       // default: false\n    allowAAAction?: boolean;        // default: false\n    rejectEncrypted?: boolean;      // default: false\n  };\n\n  svg?: {\n    allowScripts?: boolean;          // default: false\n    allowEventHandlers?: boolean;    // default: false\n    allowForeignObject?: boolean;    // default: false\n    allowExternalReferences?: boolean; // default: false\n    sanitize?: boolean;              // default: false — mutates content\n  };\n\n  csv?: {\n    preventFormulaInjection?: boolean; // default: true\n    escapeFormulas?: boolean;          // default: false — prepends \\t\n    maxLineLength?: number;            // default: 1,000,000\n  };\n\n  zip?: {\n    maxEntries?: number;               // default: 1000\n    maxUncompressedSize?: number;      // default: 524,288,000 (500 MB)\n    maxCompressionRatio?: number;      // default: 100\n    rejectNestedArchives?: boolean;    // default: false\n    rejectEncrypted?: boolean;         // default: false\n  };\n\n  image?: {\n    stripMetadata?: boolean;           // default: false (requires sharp peer dep)\n  };\n\n  json?: {\n    maxDepth?: number;                // default: 50\n    maxKeys?: number;                 // default: 10,000\n    detectPrototypePollution?: boolean; // default: true\n  };\n\n  office?: {\n    rejectMacros?: boolean;            // default: true\n    rejectEmbeddedExecutables?: boolean; // default: true\n  };\n}\n```\n\n---\n\n## Mobile Upload Behaviour\n\niOS and Android frequently upload files with an incorrect MIME type or extension. This is OS-level behaviour — not user error or an attack:\n\n| What the device sends | What the file actually is |\n|-----------------------|--------------------------|\n| `image/png` + `.png` | JPEG (camera photo) |\n| `audio/mpeg` + `.mp3` | M4A voice recording |\n| `image/jpg` + `.jpg` | JPEG (some Android versions) |\n\nBy default, `strictMimeCheck` and `strictExtensionCheck` will reject these files with `MIME_MISMATCH` / `EXTENSION_MISMATCH`. Use `mimeExceptions` to exempt known-safe cross-type pairs while keeping strict checks active for dangerous types (PDF, ZIP, executables):\n\n```js\nconst guard = createValidator({\n  strictMimeCheck: true,\n  strictExtensionCheck: true,\n\n  // Exempt common mobile upload mismatches — these are safe cross-type pairs\n  mimeExceptions: [\n    { detected: 'image/jpeg', clientMime: 'image/png' },   // iOS camera → PNG label\n    { detected: 'image/jpeg', clientMime: 'image/jpg' },   // Android jpg label\n    { detected: 'audio/mp4',  clientMime: 'audio/mpeg' },  // M4A labelled as MP3\n  ],\n});\n```\n\n**Why not just set `strictMimeCheck: false`?**\n\nA flat `false` disables the check for all types — including genuinely dangerous mismatches like a PHP webshell claiming to be `image/png`. `mimeExceptions` keeps strict checks enabled for everything except the specific pairs you explicitly trust.\n\n---\n\n## Framework Integrations\n\n### Generic Express + multer example\n\nThe most common setup — multer handles the multipart parsing and disk storage, `secure-upload` validates the saved file before your route handler runs.\n\n```js\nconst express = require('express');\nconst multer  = require('multer');\nconst { createValidator } = require('@competentgroove/secure-upload');\n\nconst app = express();\n\n// 1. Configure multer — saves uploaded files to ./uploads/\nconst upload = multer({ dest: './uploads/' });\n\n// 2. Create validator once at startup and reuse across requests\nconst fileGuard = createValidator({\n  allowedMimeTypes: [\n    'image/jpeg', 'image/png', 'image/gif',\n    'application/pdf',\n    'text/csv',\n    'audio/mp4', 'video/mp4',\n  ],\n  strictMimeCheck: true,\n  strictExtensionCheck: true,\n  mimeExceptions: [\n    { detected: 'image/jpeg', clientMime: 'image/png' }, // iOS/Android uploads\n  ],\n  pdf: { allowJavaScript: false, allowOpenAction: false },\n  csv: { preventFormulaInjection: true },\n});\n\n// 3. Reusable validation middleware — runs after multer saves the file\nasync function validateFile(req, res, next) {\n  if (!req.file) return next();\n\n  const result = await fileGuard.validateFile(req.file.path, {\n    filename: req.file.originalname,\n    mimeType: req.file.mimetype,\n  });\n\n  if (!result.valid) {\n    // Delete the rejected file — don't keep unsafe uploads on disk\n    require('fs').unlink(req.file.path, () => {});\n    return res.status(422).json({\n      error: 'File validation failed',\n      issues: result.detectedIssues.map(i => i.message),\n    });\n  }\n\n  // Attach result to request for use in route handler\n  req.fileValidation = result;\n  next();\n}\n\n// 4. Wire it up — multer first, then validate, then your handler\napp.post('/upload', upload.single('file'), validateFile, (req, res) => {\n  res.json({\n    ok: true,\n    mime: req.fileValidation.mime,\n    size: req.fileValidation.size,\n  });\n});\n\napp.listen(3000);\n```\n\n> For multiple file uploads replace `upload.single('file')` with `upload.array('files')` and loop over `req.files` in the middleware.\n\n---\n\n### Express middleware (built-in)\n\n```typescript\nimport express from 'express';\nimport multer from 'multer';\nimport { expressMiddleware } from '@competentgroove/secure-upload/middleware/express';\n\nconst upload = multer({ storage: multer.memoryStorage() });\n\napp.post(\n  '/upload',\n  upload.single('file'),\n  expressMiddleware({\n    allowedMimeTypes: ['image/png', 'image/jpeg', 'application/pdf'],\n    pdf: { allowJavaScript: false },\n  }),\n  (req, res) => {\n    // req.fileValidation has the ValidationResult\n    res.json({ ok: true, mime: req.fileValidation.mime });\n  },\n);\n\n// Handle validation errors\napp.use((err, req, res, next) => {\n  if (err.code === 'FILE_VALIDATION_FAILED') {\n    return res.status(422).json({ issues: err.results.flatMap(r => r.detectedIssues) });\n  }\n  next(err);\n});\n```\n\n### Fastify\n\n```typescript\nimport Fastify from 'fastify';\nimport { secureUploadPlugin } from '@competentgroove/secure-upload/middleware/fastify';\n\nconst fastify = Fastify();\n\nawait fastify.register(secureUploadPlugin, {\n  allowedMimeTypes: ['image/png', 'image/jpeg'],\n});\n\nfastify.post('/upload', async (request, reply) => {\n  const data = await request.file();\n  const buffer = await data.toBuffer();\n  const result = await request.validateFile(buffer, { filename: data.filename, mimeType: data.mimetype });\n\n  if (!result.valid) {\n    return reply.code(422).send({ issues: result.detectedIssues });\n  }\n  return { ok: true };\n});\n```\n\n### Koa\n\n```typescript\nimport Koa from 'koa';\nimport koaBody from 'koa-body';\nimport { koaMiddleware } from '@competentgroove/secure-upload/middleware/koa';\n\nconst app = new Koa();\n\napp.use(koaBody({ multipart: true }));\napp.use(koaMiddleware({\n  fileField: 'upload',\n  throwOnFail: true,\n  allowedMimeTypes: ['image/png', 'image/jpeg'],\n}));\n\napp.use(async (ctx) => {\n  // ctx.state.fileValidation has the result\n  ctx.body = { ok: true, result: ctx.state.fileValidation };\n});\n```\n\n---\n\n## Validation Result Format\n\n```typescript\ninterface ValidationResult {\n  valid: boolean;           // false if any non-info issue was found\n  mime: string;             // detected MIME type (from magic bytes)\n  ext: string;              // detected extension\n  format: string;           // human-readable format name (e.g., \"PDF\")\n  size: number;             // file size in bytes\n  clientMime?: string;      // client-supplied MIME (normalized)\n  clientExt?: string;       // client-supplied extension (from filename)\n  detectedIssues: ValidationIssue[];\n  sanitized?: Buffer;       // present when sanitizer was applied and changed content\n}\n\ninterface ValidationIssue {\n  code: IssueCode;          // machine-readable error code\n  severity: 'info' | 'low' | 'medium' | 'high' | 'critical';\n  message: string;          // human-readable description\n  detail?: string;          // additional context\n}\n```\n\n---\n\n## Issue Codes\n\n| Code | Severity | Description |\n|------|----------|-------------|\n| `MIME_MISMATCH` | high | Client MIME ≠ detected MIME |\n| `EXTENSION_MISMATCH` | medium | Filename ext ≠ detected ext |\n| `UNKNOWN_TYPE` | high | File type unrecognizable |\n| `TYPE_NOT_ALLOWED` | high | MIME not in allowedMimeTypes |\n| `FILE_TOO_LARGE` | high | Exceeds maxFileSize |\n| `FILE_EMPTY` | high | Zero-byte file |\n| `PDF_JAVASCRIPT` | high | `/JavaScript` or `/JS` detected |\n| `PDF_OPEN_ACTION` | high | `/OpenAction` detected |\n| `PDF_LAUNCH_ACTION` | critical | `/Launch` (executes programs) |\n| `PDF_EMBEDDED_FILE` | medium | `/EmbeddedFile` detected |\n| `PDF_XFA` | high | XFA form detected |\n| `PDF_RICH_MEDIA` | medium | Flash/video embed detected |\n| `PDF_AA_ACTION` | medium | Additional Actions detected |\n| `PDF_ENCRYPTED` | info/medium | Encrypted PDF (info by default) |\n| `PDF_MALFORMED` | high | Invalid PDF structure |\n| `PDF_TRUNCATED` | medium | Missing %%EOF |\n| `IMAGE_MALFORMED` | high | Invalid image structure |\n| `IMAGE_HEADER_INVALID` | high | Bad magic bytes / header |\n| `IMAGE_TRUNCATED` | medium | File too short |\n| `SVG_SCRIPT` | critical | `<script>` or `javascript:` URI |\n| `SVG_EVENT_HANDLER` | high | Inline event handler (on*=) |\n| `SVG_FOREIGN_OBJECT` | high | `<foreignObject>` element |\n| `SVG_EXTERNAL_REF` | medium | External URL in href |\n| `SVG_MALFORMED` | high | Not valid SVG |\n| `CSV_FORMULA_INJECTION` | high | Leading `=`, `+`, `-`, `@` |\n| `CSV_NULL_BYTES` | medium | Null bytes in content |\n| `ZIP_BOMB` | critical | Excessive size or ratio |\n| `ZIP_TOO_MANY_ENTRIES` | high | Entry count exceeded |\n| `ZIP_NESTED_ARCHIVE` | medium | Archive inside archive |\n| `ZIP_MALFORMED` | high | Invalid ZIP structure |\n| `OFFICE_MACRO` | high | VBA macro detected |\n| `OFFICE_EMBEDDED_EXEC` | critical | Embedded executable |\n| `OFFICE_MALFORMED` | high | Invalid Office file |\n| `JSON_INVALID` | high | JSON parse failure |\n| `JSON_DEPTH_EXCEEDED` | high | Nesting depth limit |\n| `JSON_KEY_LIMIT_EXCEEDED` | medium | Too many keys |\n| `JSON_PROTOTYPE_POLLUTION` | critical | `__proto__`/`constructor` keys |\n\n---\n\n## Validation Pipeline\n\n```\nInput (buffer / file / stream)\n       │\n       ▼\n[Size check] ──── too large ──▶ FILE_TOO_LARGE\n       │\n       ▼\n[Magic-byte detection] ──── unrecognized ──▶ UNKNOWN_TYPE\n       │\n       ▼\n[Allowlist check] ──── not allowed ──▶ TYPE_NOT_ALLOWED\n       │\n       ▼\n[MIME/extension comparison] ──── mismatch ──▶ MIME_MISMATCH / EXTENSION_MISMATCH\n       │\n       ▼\n[Type-specific validator(s)] ──── issues ──▶ PDF_JAVASCRIPT, SVG_SCRIPT, ZIP_BOMB, ...\n       │\n       ▼\n[Sanitizer] (optional) ──▶ result.sanitized\n       │\n       ▼\nValidationResult { valid, mime, ext, detectedIssues, sanitized? }\n```\n\n---\n\n## Plugin System\n\nRegister custom validators alongside the built-ins:\n\n```typescript\nimport { createValidator } from '@competentgroove/secure-upload';\nimport type { FileValidator, ValidationIssue, ValidatorConfig } from '@competentgroove/secure-upload';\n\nconst myValidator: FileValidator = {\n  supportedMimes: ['application/x-custom-format'],\n\n  async validate(buffer: Buffer, config: ValidatorConfig): Promise<ValidationIssue[]> {\n    const issues: ValidationIssue[] = [];\n    // Your logic here\n    return issues;\n  },\n\n  // Optional: sanitize and return cleaned buffer\n  async sanitize(buffer: Buffer, config: ValidatorConfig): Promise<Buffer> {\n    return buffer; // or return modified buffer\n  },\n};\n\nconst guard = createValidator({ allowedMimeTypes: ['application/x-custom-format'] }, [myValidator]);\n```\n\n### Planned plugin hooks (v2)\n\n- `ClamAV` — stream files through clamd for AV scanning\n- `YARA rules` — match custom threat signatures\n- `Cloud AV` — VirusTotal, Microsoft Defender Cloud, etc.\n- `PDF sanitizer` — strip dangerous elements from valid PDFs\n- `Image re-encoder` — pipe through sharp to strip metadata and re-encode\n\n---\n\n## Security Recommendations\n\nBeyond secure-upload, apply these defence-in-depth measures:\n\n1. **Store uploads outside the webroot** — never in `public/` or `static/`.\n2. **Serve user uploads from a separate origin** (or S3/CDN) with `Content-Disposition: attachment` to prevent browser execution.\n3. **Use `Content-Security-Policy`** headers to limit script execution sources.\n4. **Never preserve the original filename** on disk — generate a UUID for storage.\n5. **Quarantine before processing** — move files to a staging area, validate, then promote to production storage only if they pass.\n6. **Set short TTLs on upload endpoints** and rate-limit by IP.\n7. **For SVG files that will be rendered inline**, use the `sanitize: true` option and additionally serve them with `Content-Type: text/plain` or from an isolated origin.\n8. **For PDFs that will be rendered in-browser**, consider converting to images or using a sandboxed viewer.\n9. **Log all rejected files** — failed validations are high-value security events.\n10. **Run secure-upload in a Worker Thread** or separate process for untrusted file processing to limit blast radius if a parser crashes.\n\n---\n\n## Roadmap\n\n- [ ] ZIP64 support (files > 4 GB)\n- [ ] Streaming validators (avoid loading large files fully)\n- [ ] ClamAV integration\n- [ ] YARA rule matching\n- [ ] PDF sanitization (strip JS without rejecting the file)\n- [ ] Image re-encoding via sharp (metadata stripping)\n- [ ] NestJS pipe integration\n- [ ] Content-Disarm-and-Reconstruct (CDR) pipeline\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md"}