diff --git a/package-lock.json b/package-lock.json index f0b9ec5f805..ea51a477ca9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@fastify/static": "^10.0.0", "@inquirer/prompts": "^8.7.2", "@netlify/ai": "^1.0.1", "@netlify/api": "^15.1.2", @@ -56,7 +55,6 @@ "express": "^5.2.1", "express-logging": "^1.1.1", "fastest-levenshtein": "^1.0.16", - "fastify": "^5.8.5", "find-up": "^8.0.0", "folder-walker": "^3.2.0", "fuzzy": "^0.1.3", @@ -1391,257 +1389,12 @@ ], "license": "MIT" }, - "node_modules/@fastify/ajv-compiler": { - "version": "4.0.5", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "fast-uri": "^3.0.0" - } - }, "node_modules/@fastify/busboy": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.1.tgz", "integrity": "sha512-tgK4O+57iz5ycYNGXE5ZWj1ES03lD2XnnBYWSbU/3wYZRMQzCUq7Ycds/RdyBZQeL5MU4fxBt6lzbIWf/Bickw==", "license": "MIT" }, - "node_modules/@fastify/error": { - "version": "4.2.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/@fastify/fast-json-stringify-compiler": { - "version": "5.0.3", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "fast-json-stringify": "^6.0.0" - } - }, - "node_modules/@fastify/forwarded": { - "version": "3.0.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/@fastify/merge-json-schemas": { - "version": "0.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/@fastify/proxy-addr": { - "version": "5.1.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/forwarded": "^3.0.0", - "ipaddr.js": "^2.1.0" - } - }, - "node_modules/@fastify/proxy-addr/node_modules/ipaddr.js": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", - "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/@fastify/send": { - "version": "4.1.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@lukeed/ms": "^2.0.2", - "escape-html": "~1.0.3", - "fast-decode-uri-component": "^1.0.1", - "http-errors": "^2.0.0", - "mime": "^3" - } - }, - "node_modules/@fastify/static": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/@fastify/static/-/static-10.1.2.tgz", - "integrity": "sha512-G/g18cG9tLutT/OVyN1AIsHIl9L1UwmJ+S3dkyhVpplIx0nEMicd7RGQ+uJLyhKKF4a3tTcQydccn3Mop1fX+Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/accept-negotiator": "^2.0.0", - "@fastify/error": "^4.0.0", - "@fastify/send": "^4.0.0", - "content-disposition": "^2.0.1", - "fastify-plugin": "^6.0.0", - "fastq": "^1.17.1", - "glob": "^13.0.0" - } - }, - "node_modules/@fastify/static/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@fastify/static/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@fastify/static/node_modules/content-disposition": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz", - "integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@fastify/static/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@fastify/static/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@fastify/static/node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@fastify/static/node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "dev": true, @@ -3030,13 +2783,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@lukeed/ms": { - "version": "2.0.2", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@mapbox/node-pre-gyp": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", @@ -6441,6 +6187,7 @@ }, "node_modules/@pinojs/redact": { "version": "0.4.0", + "dev": true, "license": "MIT" }, "node_modules/@pkgjs/parseargs": { @@ -9220,10 +8967,6 @@ "node": ">=6.5" } }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "license": "MIT" - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -9307,21 +9050,6 @@ "ajv": "^8.0.1" } }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/ansi-align": { "version": "3.0.1", "license": "ISC", @@ -9665,6 +9393,7 @@ }, "node_modules/atomic-sleep": { "version": "1.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8.0.0" @@ -9692,14 +9421,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/avvio": { - "version": "9.1.0", - "license": "MIT", - "dependencies": { - "@fastify/error": "^4.0.0", - "fastq": "^1.17.1" - } - }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -11339,13 +11060,6 @@ "node": ">= 0.8" } }, - "node_modules/dequal": { - "version": "2.0.3", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", @@ -12864,10 +12578,6 @@ ], "license": "MIT" }, - "node_modules/fast-decode-uri-component": { - "version": "1.0.1", - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "license": "MIT" @@ -12912,40 +12622,11 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-json-stringify": { - "version": "6.2.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/merge-json-schemas": "^0.2.0", - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "fast-uri": "^3.0.0", - "json-schema-ref-resolver": "^3.0.0", - "rfdc": "^1.2.0" - } - }, "node_modules/fast-levenshtein": { "version": "2.0.6", "dev": true, "license": "MIT" }, - "node_modules/fast-querystring": { - "version": "1.1.2", - "license": "MIT", - "dependencies": { - "fast-decode-uri-component": "^1.0.1" - } - }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "license": "MIT" @@ -13001,95 +12682,6 @@ "node": ">= 4.9.1" } }, - "node_modules/fastify": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.12.1.tgz", - "integrity": "sha512-FWi+tQvwxR/PeRX7Z2mhfEF5ozJ3jn9asiiclzKXNSzJRHAYcU924aIOKAdHFJ+YIKieh3cqr1IwCOvTr41B3Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/ajv-compiler": "^4.0.5", - "@fastify/error": "^4.0.0", - "@fastify/fast-json-stringify-compiler": "^5.0.0", - "@fastify/proxy-addr": "^5.0.0", - "abstract-logging": "^2.0.1", - "avvio": "^9.0.0", - "fast-json-stringify": "^7.0.0", - "find-my-way": "^9.6.0", - "light-my-request": "^6.0.0", - "pino": "^9.14.0 || ^10.1.0", - "process-warning": "^5.1.0", - "rfdc": "^1.3.1", - "secure-json-parse": "^4.0.0", - "semver": "^7.6.0", - "toad-cache": "^3.7.0" - } - }, - "node_modules/fastify-plugin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.0.tgz", - "integrity": "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/fastify/node_modules/fast-json-stringify": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", - "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/merge-json-schemas": "^0.2.0", - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "fast-uri": "^4.0.0", - "json-schema-ref-resolver": "^3.0.0", - "rfdc": "^1.2.0" - } - }, - "node_modules/fastify/node_modules/fast-uri": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.4.tgz", - "integrity": "sha512-dODXrIxlS9JSdgAnhIUKOosKV1oMtU2VtVw87QRaHzyl5jxO290Ii5tEZfCfzfWNHi3jKWwBSdQj0qIyshdZdQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fastq": { "version": "1.19.1", "license": "ISC", @@ -13231,20 +12823,6 @@ "node": ">= 0.8" } }, - "node_modules/find-my-way": { - "version": "9.7.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz", - "integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-querystring": "^1.0.0", - "safe-regex2": "^5.0.0" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/find-up": { "version": "8.0.0", "license": "MIT", @@ -15252,23 +14830,6 @@ "dev": true, "license": "(AFL-2.1 OR BSD-3-Clause)" }, - "node_modules/json-schema-ref-resolver": { - "version": "3.0.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "license": "MIT" @@ -15533,52 +15094,6 @@ "node": ">= 0.8.0" } }, - "node_modules/light-my-request": { - "version": "6.6.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause", - "dependencies": { - "cookie": "^1.0.1", - "process-warning": "^4.0.0", - "set-cookie-parser": "^2.6.0" - } - }, - "node_modules/light-my-request/node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/light-my-request/node_modules/process-warning": { - "version": "4.0.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, "node_modules/listhen": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/listhen/-/listhen-1.10.1.tgz", @@ -16145,6 +15660,7 @@ }, "node_modules/mime": { "version": "3.0.0", + "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -16719,6 +16235,7 @@ }, "node_modules/on-exit-leak-free": { "version": "2.1.2", + "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" @@ -17469,6 +16986,7 @@ }, "node_modules/pino": { "version": "9.14.0", + "dev": true, "license": "MIT", "dependencies": { "@pinojs/redact": "^0.4.0", @@ -17510,10 +17028,12 @@ }, "node_modules/pino-std-serializers": { "version": "7.0.0", + "dev": true, "license": "MIT" }, "node_modules/pino/node_modules/pino-abstract-transport": { "version": "2.0.0", + "dev": true, "license": "MIT", "dependencies": { "split2": "^4.0.0" @@ -17521,6 +17041,7 @@ }, "node_modules/pino/node_modules/split2": { "version": "4.2.0", + "dev": true, "license": "ISC", "engines": { "node": ">= 10.x" @@ -17734,6 +17255,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "dev": true, "funding": [ { "type": "github", @@ -17869,6 +17391,7 @@ }, "node_modules/quick-format-unescaped": { "version": "4.0.4", + "dev": true, "license": "MIT" }, "node_modules/quick-lru": { @@ -18223,6 +17746,7 @@ }, "node_modules/real-require": { "version": "0.2.0", + "dev": true, "license": "MIT", "engines": { "node": ">= 12.13.0" @@ -18412,13 +17936,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ret": { - "version": "0.5.0", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/retry": { "version": "0.13.1", "license": "MIT", @@ -18628,23 +18145,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-regex2": { - "version": "5.0.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "ret": "~0.5.0" - } - }, "node_modules/safe-stable-stringify": { "version": "2.5.0", "license": "MIT", @@ -18675,20 +18175,6 @@ "node": ">=11.0.0" } }, - "node_modules/secure-json-parse": { - "version": "4.1.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -18813,10 +18299,6 @@ "node": ">= 0.8.0" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "license": "MIT" - }, "node_modules/set-error-message": { "version": "2.0.1", "license": "MIT", @@ -19071,6 +18553,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "dev": true, "license": "MIT", "dependencies": { "atomic-sleep": "^1.0.0" @@ -19657,6 +19140,7 @@ }, "node_modules/thread-stream": { "version": "3.1.0", + "dev": true, "license": "MIT", "dependencies": { "real-require": "^0.2.0" @@ -19806,13 +19290,6 @@ "node": ">=8.0" } }, - "node_modules/toad-cache": { - "version": "3.7.0", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "license": "MIT", diff --git a/package.json b/package.json index 5fea6e34d0a..961a253b783 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,6 @@ "typecheck:watch": "node ./node_modules/typescript-native/bin/tsc --watch" }, "dependencies": { - "@fastify/static": "^10.0.0", "@inquirer/prompts": "^8.7.2", "@netlify/ai": "^1.0.1", "@netlify/api": "^15.1.2", @@ -98,7 +97,6 @@ "express": "^5.2.1", "express-logging": "^1.1.1", "fastest-levenshtein": "^1.0.16", - "fastify": "^5.8.5", "find-up": "^8.0.0", "folder-walker": "^3.2.0", "fuzzy": "^0.1.3", diff --git a/src/utils/static-server.ts b/src/utils/static-server.ts index 87a73ade016..44978d21fc4 100644 --- a/src/utils/static-server.ts +++ b/src/utils/static-server.ts @@ -1,40 +1,140 @@ +import { lookup } from 'dns/promises' +import { stat } from 'fs/promises' +import http from 'http' +import type { AddressInfo } from 'net' import path from 'path' -import fastifyStatic from '@fastify/static' -import Fastify from 'fastify' +import express from 'express' import { log, NETLIFYDEVLOG } from './command-helpers.js' +import type { ServerSettings } from './types.js' -/** - * @param {object} config - * @param {import('./types.js').ServerSettings} config.settings - */ -// @ts-expect-error TS(7031) FIXME: Binding element 'settings' implicitly has an 'any'... Remove this comment to see the full error message -export const startStaticServer = async ({ settings }) => { - const server = Fastify() - const rootPath = path.resolve(settings.dist) - server.register(fastifyStatic, { - root: rootPath, - etag: false, - acceptRanges: false, - lastModified: false, +const ALLOWED_METHODS = new Set(['GET', 'HEAD']) +const LOCALHOST = 'localhost' +const GENERATED_RESPONSE_CACHE_CONTROL = 'public, max-age=0, must-revalidate' +const FILE_CACHE_CONTROL = 'public, max-age=0' +const ENCODED_SLASH = /%2f/i + +const FILE_OPTIONS = { + acceptRanges: false, + dotfiles: 'allow', + etag: false, + lastModified: false, +} as const + +const listen = (server: http.Server, port: number | undefined, host: string) => + new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(port, host, () => { + server.off('error', reject) + resolve(server.address() as AddressInfo) + }) }) - server.setNotFoundHandler((_req, res) => { - res.code(404).sendFile('404.html', rootPath) +const getErrorStatus = (error: unknown) => + error instanceof Object && 'status' in error && typeof error.status === 'number' ? error.status : 500 + +const isDirectoryInRoot = async (rootPath: string, decodedPath: string) => { + const filePath = path.resolve(rootPath, `.${decodedPath}`) + const relativePath = path.relative(rootPath, filePath) + if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { + return false + } + try { + return (await stat(filePath)).isDirectory() + } catch { + return false + } +} + +const createApp = (rootPath: string) => { + const app = express() + app.disable('x-powered-by') + app.disable('etag') + + const serveFile = express.static(rootPath, { + ...FILE_OPTIONS, + redirect: false, + setHeaders: (res) => { + res.setHeader('cache-control', FILE_CACHE_CONTROL) + }, }) - server.addHook('onRequest', (req, reply, done) => { - reply.header('age', '0') - reply.header('cache-control', 'public, max-age=0, must-revalidate') - const validMethods = ['GET', 'HEAD'] - if (!validMethods.includes(req.method)) { - reply.code(405).send('Method Not Allowed') + app.use(async (req, res, next) => { + let decodedPath: string + try { + decodedPath = decodeURIComponent(req.path) + } catch { + res.status(400).type('text/plain').send('Bad Request') + return } - done() + + res.setHeader('age', '0') + res.setHeader('cache-control', GENERATED_RESPONSE_CACHE_CONTROL) + if (!ALLOWED_METHODS.has(req.method)) { + res.status(405).type('text/plain').send('Method Not Allowed') + return + } + if (decodedPath.includes('\0')) { + res.status(400).type('text/plain').send('Bad Request') + return + } + // Encoded slashes aren't decoded into path separators, so they never match a file. + if (ENCODED_SLASH.test(req.path)) { + next() + return + } + if (decodedPath.split('/').includes('..')) { + res.status(403).type('text/plain').send('Forbidden') + return + } + + // No validators are sent, so conditional requests always get the full file. + delete req.headers['if-none-match'] + delete req.headers['if-modified-since'] + // Directories without a trailing slash serve their index.html directly instead of redirecting. + if (!req.path.endsWith('/') && (await isDirectoryInRoot(rootPath, decodedPath))) { + req.url = req.url.replace(req.path, `${req.path}/`) + } + serveFile(req, res, next) + }) + + app.use((_req, res) => { + const headers = { 'cache-control': FILE_CACHE_CONTROL } + res.status(404).sendFile('404.html', { ...FILE_OPTIONS, headers, root: rootPath }, (error: Error | undefined) => { + if (error && !res.headersSent) { + res.status(404).type('text/plain').send('404 Not Found') + } + }) }) - await server.listen({ port: settings.frameworkPort }) - const [address] = server.addresses() - log(`\n${NETLIFYDEVLOG} Static server listening to`, settings.frameworkPort) - return { family: address.family } + + app.use((error: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => { + const status = error instanceof URIError ? 400 : getErrorStatus(error) + res.status(status).type('text/plain').send(http.STATUS_CODES[status]) + }) + + return app +} + +export const startStaticServer = async ({ settings }: { settings: Pick }) => { + const app = createApp(path.resolve(settings.dist)) + + const mainAddress = await listen(http.createServer(app), settings.frameworkPort, LOCALHOST) + const additionalAddresses: AddressInfo[] = [] + const localhostAddresses = await lookup(LOCALHOST, { all: true }).catch(() => []) + for (const { address } of localhostAddresses) { + if (address !== mainAddress.address) { + try { + additionalAddresses.push(await listen(http.createServer(app), mainAddress.port, address)) + } catch { + // Localhost addresses that can't be bound, e.g. with IPv6 disabled, are skipped. + } + } + } + + log(`\n${NETLIFYDEVLOG} Static server listening to`, String(settings.frameworkPort)) + // The dev proxy connects using this family. Reporting the first additional binding keeps it on 127.0.0.1 + // when localhost resolves to ::1 first. + const [reportedAddress] = [...additionalAddresses, mainAddress] + return { family: reportedAddress.family } } diff --git a/tests/unit/commands/database/db-migration-pull.test.ts b/tests/unit/commands/database/db-migration-pull.test.ts index 8febc268a6c..f5288df1f86 100644 --- a/tests/unit/commands/database/db-migration-pull.test.ts +++ b/tests/unit/commands/database/db-migration-pull.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterEach, afterAll } from 'vitest' const { mockRm, mockMkdir, mockWriteFile, mockFetch, mockExeca, logMessages, jsonMessages } = vi.hoisted(() => { const mockRm = vi.fn().mockResolvedValue(undefined) @@ -42,6 +42,10 @@ vi.mock('../../../../src/utils/execa.js', () => ({ })) vi.stubGlobal('fetch', mockFetch) +// Test files share one thread, so a leftover stub would leak into later files. +afterAll(() => { + vi.unstubAllGlobals() +}) import { resolve } from 'path' diff --git a/tests/unit/commands/database/db-migrations-reset.test.ts b/tests/unit/commands/database/db-migrations-reset.test.ts index 1c46d3ba031..d328549bb13 100644 --- a/tests/unit/commands/database/db-migrations-reset.test.ts +++ b/tests/unit/commands/database/db-migrations-reset.test.ts @@ -1,6 +1,6 @@ import { join } from 'path' -import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterEach, afterAll } from 'vitest' const { mockCleanup, mockExecutor, mockQuery, mockReaddir, mockRm, mockFetch, logMessages, jsonMessages } = vi.hoisted( () => { @@ -47,6 +47,10 @@ vi.mock('../../../../src/utils/command-helpers.js', async () => ({ })) vi.stubGlobal('fetch', mockFetch) +// Test files share one thread, so a leftover stub would leak into later files. +afterAll(() => { + vi.unstubAllGlobals() +}) import { migrationsReset } from '../../../../src/commands/database/db-migrations-reset.js' diff --git a/tests/unit/commands/database/db-status.test.ts b/tests/unit/commands/database/db-status.test.ts index 651d3008a57..ffbb090138a 100644 --- a/tests/unit/commands/database/db-status.test.ts +++ b/tests/unit/commands/database/db-status.test.ts @@ -1,6 +1,6 @@ import { relative, sep } from 'path' -import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' +import { describe, expect, test, vi, beforeEach, afterEach, afterAll } from 'vitest' const { mockReaddir, @@ -85,6 +85,10 @@ vi.mock('../../../../src/commands/database/util/db-connection.js', () => ({ })) vi.stubGlobal('fetch', mockFetch) +// Test files share one thread, so a leftover stub would leak into later files. +afterAll(() => { + vi.unstubAllGlobals() +}) import { statusDb } from '../../../../src/commands/database/db-status.js' diff --git a/tests/unit/utils/static-server.test.ts b/tests/unit/utils/static-server.test.ts new file mode 100644 index 00000000000..e37ddf3563c --- /dev/null +++ b/tests/unit/utils/static-server.test.ts @@ -0,0 +1,214 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises' +import { lookup } from 'dns/promises' +import net from 'net' +import { tmpdir } from 'os' +import { join } from 'path' + +import getPort from 'get-port' +import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest' + +import { startStaticServer } from '../../../src/utils/static-server.js' + +vi.mock('../../../src/utils/command-helpers.js', async () => ({ + ...(await vi.importActual('../../../src/utils/command-helpers.js')), + log: vi.fn(), +})) + +const FILE_CACHE_CONTROL = 'public, max-age=0' +const GENERATED_RESPONSE_CACHE_CONTROL = 'public, max-age=0, must-revalidate' +const HTML = 'text/html; charset=utf-8' +const PLAIN = 'text/plain; charset=utf-8' +const CUSTOM_404 = '

custom 404

' + +const createSite = async ({ with404Page }: { with404Page: boolean }) => { + const parent = await mkdtemp(join(tmpdir(), 'static-server-')) + await writeFile(join(parent, 'outside.txt'), 'outside') + await mkdir(join(parent, 'outside-dir')) + const root = join(parent, 'site') + await mkdir(root) + await writeFile(join(root, 'index.html'), '

home

') + await writeFile(join(root, 'style.css'), 'body{}') + await writeFile(join(root, 'data.json'), '{"a":1}') + await writeFile(join(root, 'noext'), 'plain') + await writeFile(join(root, '.hidden'), 'dotfile') + await writeFile(join(root, 'file with space.txt'), 'spaced') + await mkdir(join(root, 'sub')) + await writeFile(join(root, 'sub', 'index.html'), '

sub

') + await mkdir(join(root, 'empty')) + if (with404Page) { + await writeFile(join(root, '404.html'), CUSTOM_404) + } + return { parent, root } +} + +// fetch normalizes `..` segments away, so traversal attempts need a raw request. +const sendRawRequest = (port: number, rawPath: string) => + new Promise<{ status: number; body: string }>((resolve, reject) => { + const socket = net.connect({ host: '127.0.0.1', port }, () => { + socket.write(`GET ${rawPath} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n`) + }) + let response = '' + socket.on('data', (chunk) => { + response += chunk.toString() + }) + socket.on('end', () => { + const [head, body = ''] = response.split('\r\n\r\n') + resolve({ status: Number(head.split(' ')[1]), body }) + }) + socket.on('error', reject) + }) + +const canConnect = (host: string, port: number) => + new Promise((resolve) => { + const socket = net.connect({ host, port }, () => { + socket.destroy() + resolve(true) + }) + socket.on('error', () => { + resolve(false) + }) + }) + +describe('startStaticServer', () => { + const sites: string[] = [] + let baseUrl: string + let port: number + let family: string + + beforeAll(async () => { + const { parent, root } = await createSite({ with404Page: true }) + sites.push(parent) + port = await getPort() + ;({ family } = await startStaticServer({ settings: { dist: root, frameworkPort: port } })) + baseUrl = `http://127.0.0.1:${String(port)}` + }) + + afterAll(async () => { + await Promise.all(sites.map((site) => rm(site, { recursive: true, force: true }))) + }) + + test.each([ + { path: '/', contentType: HTML, body: '

home

' }, + { path: '/index.html', contentType: HTML, body: '

home

' }, + { path: '/style.css', contentType: 'text/css; charset=utf-8', body: 'body{}' }, + { path: '/data.json', contentType: 'application/json; charset=utf-8', body: '{"a":1}' }, + { path: '/noext', contentType: 'application/octet-stream', body: 'plain' }, + { path: '/.hidden', contentType: 'application/octet-stream', body: 'dotfile' }, + { path: '/file%20with%20space.txt', contentType: PLAIN, body: 'spaced' }, + { path: '/sub', contentType: HTML, body: '

sub

' }, + { path: '/sub/', contentType: HTML, body: '

sub

' }, + { path: '/style.css?v=2', contentType: 'text/css; charset=utf-8', body: 'body{}' }, + ])('serves $path with status 200 and no redirect', async ({ path, contentType, body }) => { + const response = await fetch(`${baseUrl}${path}`, { redirect: 'manual' }) + + expect(response.status).toBe(200) + expect(response.headers.get('content-type')).toBe(contentType) + expect(response.headers.get('cache-control')).toBe(FILE_CACHE_CONTROL) + expect(response.headers.get('age')).toBe('0') + expect(await response.text()).toBe(body) + }) + + test.each(['/missing', '/missing.css', '/empty', '/empty/', '/../etc/passwd', '/%2e%2e/%2e%2e/etc/passwd'])( + 'serves the custom 404 page for %s', + async (path) => { + const response = await fetch(`${baseUrl}${path}`) + + expect(response.status).toBe(404) + expect(response.headers.get('content-type')).toBe(HTML) + expect(response.headers.get('cache-control')).toBe(FILE_CACHE_CONTROL) + expect(await response.text()).toBe(CUSTOM_404) + }, + ) + + test.each(['POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])('rejects %s with 405', async (method) => { + const response = await fetch(`${baseUrl}/`, { method }) + + expect(response.status).toBe(405) + expect(response.headers.get('content-type')).toBe(PLAIN) + expect(response.headers.get('cache-control')).toBe(GENERATED_RESPONSE_CACHE_CONTROL) + expect(await response.text()).toBe('Method Not Allowed') + }) + + test('answers HEAD requests without a body', async () => { + const response = await fetch(`${baseUrl}/`, { method: 'HEAD' }) + + expect(response.status).toBe(200) + expect(response.headers.get('content-length')).toBe(String('

home

'.length)) + expect(await response.text()).toBe('') + }) + + test.each<{ name: string; headers: Record }>([ + { name: 'range requests', headers: { range: 'bytes=0-1' } }, + { name: 'If-None-Match', headers: { 'if-none-match': '*' } }, + { name: 'If-Modified-Since', headers: { 'if-modified-since': new Date(Date.now() + 86_400_000).toUTCString() } }, + ])('ignores $name and returns the full file', async ({ headers }) => { + const response = await fetch(`${baseUrl}/style.css`, { headers }) + + expect(response.status).toBe(200) + expect(await response.text()).toBe('body{}') + }) + + test('serves the 404 page for an encoded slash instead of decoding it into a path', async () => { + const response = await fetch(`${baseUrl}/sub%2Findex.html`) + + expect(response.status).toBe(404) + expect(await response.text()).toBe(CUSTOM_404) + }) + + test('rejects a null byte in the path with 400', async () => { + const response = await fetch(`${baseUrl}/style%00.css`) + + expect(response.status).toBe(400) + expect(response.headers.get('cache-control')).toBe(GENERATED_RESPONSE_CACHE_CONTROL) + }) + + test.each(['/%', '/%E0%A4%A'])('rejects the malformed path %s with 400 before adding cache headers', async (path) => { + const response = await fetch(`${baseUrl}${path}`) + + expect(response.status).toBe(400) + expect(response.headers.get('cache-control')).toBeNull() + expect(response.headers.get('age')).toBeNull() + }) + + test.each(['/../outside.txt', '/../outside-dir', '/%2e%2e/outside.txt', '/sub/../index.html'])( + 'rejects the path %s containing a parent segment with 403', + async (rawPath) => { + const response = await sendRawRequest(port, rawPath) + + expect(response.status).toBe(403) + expect(response.body).not.toContain('outside') + }, + ) + + test('does not send validators, range support or a framework banner', async () => { + const response = await fetch(`${baseUrl}/style.css`) + + expect(response.headers.get('etag')).toBeNull() + expect(response.headers.get('last-modified')).toBeNull() + expect(response.headers.get('accept-ranges')).toBeNull() + expect(response.headers.get('x-powered-by')).toBeNull() + }) + + test('listens on every localhost address and reports the first additional one', async () => { + const addresses = await lookup('localhost', { all: true }) + const [mainAddress, firstAdditionalAddress = mainAddress] = addresses + + expect(family).toBe(`IPv${String(firstAdditionalAddress.family)}`) + for (const { address } of addresses) { + expect(await canConnect(address, port)).toBe(true) + } + }) + + test('falls back to a plain-text 404 when the site has no 404.html', async () => { + const { parent, root } = await createSite({ with404Page: false }) + sites.push(parent) + const otherPort = await getPort() + await startStaticServer({ settings: { dist: root, frameworkPort: otherPort } }) + + const response = await fetch(`http://127.0.0.1:${String(otherPort)}/missing`) + + expect(response.status).toBe(404) + expect(response.headers.get('content-type')).toBe(PLAIN) + expect(await response.text()).toBe('404 Not Found') + }) +})