diff --git a/.changeset/daring-newts-squeak.md b/.changeset/daring-newts-squeak.md new file mode 100644 index 000000000..67bb21407 --- /dev/null +++ b/.changeset/daring-newts-squeak.md @@ -0,0 +1,5 @@ +--- +type: Security +pr: 4121 +--- +**Removed a critical unpatched supply-chain vulnerability from the `lint:ci` toolchain** — the `shellcheck` devDependency pulled in `decompress@4.2.1`, which carries an unpatched critical zip-slip flaw (GHSA-mp2f-45pm-3cg9); replaced with a small dependency-free downloader that fetches a pinned ShellCheck release directly and extracts it without the vulnerable extraction library. (#4120) diff --git a/bin/install.js b/bin/install.js index 0bc641766..203417f9b 100755 --- a/bin/install.js +++ b/bin/install.js @@ -411,7 +411,7 @@ const GSD_CHANGESET_FILES = [ 'github-release-notes.cjs', 'lint.cjs', 'new.cjs', 'README.md', // documentation only — not user-authored ]; -const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs', 'drift-scan.cjs', 'alias-drift-families.cjs', 'exit-code-registry.cjs', 'ndjson-reporter.cjs', 'ci-job-timing.cjs']; +const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs', 'drift-scan.cjs', 'alias-drift-families.cjs', 'exit-code-registry.cjs', 'ndjson-reporter.cjs', 'ci-job-timing.cjs', 'shellcheck-fetch.cjs']; /** * Resolve a runtime's shared-hooks directory name from its descriptor. diff --git a/package-lock.json b/package-lock.json index 776121005..0ba183a1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,6 @@ "js-yaml": "^4.3.1", "mutation-testing-metrics": "^3.7.3", "re2js": "^2.8.6", - "shellcheck": "^4.1.0", "typescript": "^6.0.3", "typescript-eslint": "^8.60.0" }, @@ -755,17 +754,6 @@ "node": ">=18" } }, - "node_modules/@borewit/text-codec": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", - "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -1113,35 +1101,6 @@ "win32" ] }, - "node_modules/@felipecrs/decompress-tarxz": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@felipecrs/decompress-tarxz/-/decompress-tarxz-5.0.4.tgz", - "integrity": "sha512-a+nAnDsiUA84Sy/a+FKYJtjOjFvNtW8Jcbi3NwE8kJKPpYAxINFLYsC9mev9/wngiNEBA3jfHn0qNFwICeZNJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xhmikosr/decompress-tar": "^8.1.0", - "file-type": "^20.5.0", - "is-stream": "^2.0.1", - "xz-decompress": "^0.2.3" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@felipecrs/decompress-tarxz/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@hono/node-server": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.11.tgz", @@ -1816,32 +1775,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/@tokenizer/inflate": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", - "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "fflate": "^0.8.2", - "token-types": "^6.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -2116,62 +2049,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@xhmikosr/decompress-tar": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-tar/-/decompress-tar-8.1.0.tgz", - "integrity": "sha512-m0q8x6lwxenh1CrsTby0Jrjq4vzW/QU1OLhTHMQLEdHpmjR1lgahGz++seZI0bXF3XcZw3U3xHfqZSz+JPP2Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "file-type": "^20.5.0", - "is-stream": "^2.0.1", - "tar-stream": "^3.1.7" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@xhmikosr/decompress-tar/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@xhmikosr/decompress-unzip": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-unzip/-/decompress-unzip-7.1.0.tgz", - "integrity": "sha512-oqTYAcObqTlg8owulxFTqiaJkfv2SHsxxxz9Wg4krJAHVzGWlZsU8tAB30R6ow+aHrfv4Kub6WQ8u04NWVPUpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "file-type": "^20.5.0", - "get-stream": "^6.0.1", - "yauzl": "^3.1.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@xhmikosr/decompress-unzip/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -2284,37 +2161,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/b4a": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", - "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -2325,112 +2171,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.1.tgz", - "integrity": "sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", - "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.2.tgz", - "integrity": "sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-path": "^3.0.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/baseline-browser-mapping": { "version": "2.10.32", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", @@ -2444,17 +2184,6 @@ "node": ">=6.0.0" } }, - "node_modules/bl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", - "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -2492,14 +2221,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", @@ -2547,66 +2268,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "node_modules/buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "dev": true, - "license": "MIT" - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", - "dev": true, - "license": "MIT" - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2650,25 +2311,6 @@ } } }, - "node_modules/call-bind": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", - "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "get-intrinsic": "^1.3.0", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2858,13 +2500,6 @@ "node": ">=6.6.0" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -2913,226 +2548,6 @@ } } }, - "node_modules/decompress": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", - "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "decompress-tar": "^4.0.0", - "decompress-tarbz2": "^4.0.0", - "decompress-targz": "^4.0.0", - "decompress-unzip": "^4.0.1", - "graceful-fs": "^4.1.10", - "make-dir": "^1.0.0", - "pify": "^2.3.0", - "strip-dirs": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-tar": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", - "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "file-type": "^5.2.0", - "is-stream": "^1.1.0", - "tar-stream": "^1.5.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-tar/node_modules/file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-tar/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decompress-tar/node_modules/tar-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/decompress-tarbz2": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", - "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "decompress-tar": "^4.1.0", - "file-type": "^6.1.0", - "is-stream": "^1.1.0", - "seek-bzip": "^1.0.5", - "unbzip2-stream": "^1.0.9" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-tarbz2/node_modules/file-type": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", - "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-tarbz2/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decompress-targz": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", - "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "decompress-tar": "^4.1.1", - "file-type": "^5.2.0", - "is-stream": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-targz/node_modules/file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-targz/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decompress-unzip": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", - "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "file-type": "^3.8.0", - "get-stream": "^2.2.0", - "pify": "^2.3.0", - "yauzl": "^2.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-unzip/node_modules/file-type": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decompress-unzip/node_modules/get-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", - "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4.0.1", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decompress-unzip/node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/decompress/node_modules/make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress/node_modules/make-dir/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3140,42 +2555,6 @@ "dev": true, "license": "MIT" }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3206,13 +2585,6 @@ "node": ">=8" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT" - }, "node_modules/diff-match-patch": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", @@ -3263,16 +2635,6 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.22.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz", @@ -3287,19 +2649,6 @@ "node": ">=10.13.0" } }, - "node_modules/envalid": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/envalid/-/envalid-8.1.0.tgz", - "integrity": "sha512-OT6+qVhKVyCidaGoXflb2iK1tC8pd0OV2Q+v9n33wNhUJ+lus+rJobUj4vJaQBPxPZ0vYrPGuxdrenyCAIJcow==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "2.8.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3330,13 +2679,6 @@ "node": ">= 0.4" } }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "license": "MIT" - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3699,16 +3041,6 @@ "node": ">=12" } }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -3876,13 +3208,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3940,16 +3265,6 @@ "fast-string-width": "^3.0.2" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -3968,13 +3283,6 @@ } } }, - "node_modules/fflate": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", - "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "dev": true, - "license": "MIT" - }, "node_modules/figures": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", @@ -4004,25 +3312,6 @@ "node": ">=16.0.0" } }, - "node_modules/file-type": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.5.0.tgz", - "integrity": "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tokenizer/inflate": "^0.2.6", - "strtok3": "^10.2.0", - "token-types": "^6.0.0", - "uint8array-extras": "^1.4.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -4082,22 +3371,6 @@ "dev": true, "license": "ISC" }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -4133,13 +3406,6 @@ "node": ">= 0.8" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT" - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4267,24 +3533,6 @@ "node": ">=10.13.0" } }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, "node_modules/globals": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", @@ -4298,23 +3546,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", @@ -4351,19 +3582,6 @@ "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -4376,22 +3594,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", @@ -4466,27 +3668,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4548,19 +3729,6 @@ "node": ">= 0.10" } }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4594,13 +3762,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-natural-number": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", - "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", - "dev": true, - "license": "MIT" - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -4633,22 +3794,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-unicode-supported": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", @@ -4662,13 +3807,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4819,13 +3957,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4919,19 +4050,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5150,16 +4268,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -5312,13 +4420,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5339,39 +4440,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pkce-challenge": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", @@ -5381,16 +4449,6 @@ "node": ">=16.20.0" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5417,13 +4475,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -5523,29 +4574,6 @@ "node": ">=18.0.0" } }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5585,24 +4613,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -5629,54 +4639,12 @@ "tslib": "^2.1.0" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/seek-bzip": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", - "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^2.8.1" - }, - "bin": { - "seek-bunzip": "bin/seek-bunzip", - "seek-table": "bin/seek-bzip-table" - } - }, - "node_modules/seek-bzip/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -5690,13 +4658,6 @@ "node": ">=10" } }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "license": "MIT" - }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -5723,22 +4684,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/serve-static": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", @@ -5758,24 +4703,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -5803,26 +4730,6 @@ "node": ">=8" } }, - "node_modules/shellcheck": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/shellcheck/-/shellcheck-4.1.0.tgz", - "integrity": "sha512-8143z6YGO4+Puwp9Ghn/g7+QxllSKlXaZSm3HXfvQXUfRXhM5P8TPORRHBBlyobl9BnniVne+d1Ff6RgNiccsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@felipecrs/decompress-tarxz": "5.0.4", - "@xhmikosr/decompress-unzip": "7.1.0", - "decompress": "4.2.1", - "envalid": "8.1.0", - "global-agent": "3.0.0" - }, - "bin": { - "shellcheck": "bin/shellcheck.js" - }, - "engines": { - "node": ">=20.9.0" - } - }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -5918,13 +4825,6 @@ "node": ">= 12" } }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -5934,35 +4834,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5991,16 +4862,6 @@ "node": ">=8" } }, - "node_modules/strip-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", - "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-natural-number": "^4.0.1" - } - }, "node_modules/strip-final-newline": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", @@ -6027,23 +4888,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strtok3": { - "version": "10.3.5", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", - "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6102,29 +4946,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - } - }, "node_modules/test-exclude": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", @@ -6140,23 +4961,6 @@ "node": "20 || >=22" } }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true, - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -6174,28 +4978,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/to-buffer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", - "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "isarray": "^2.0.5", - "safe-buffer": "^5.2.1", - "typed-array-buffer": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/to-buffer/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -6205,25 +4987,6 @@ "node": ">=0.6" } }, - "node_modules/token-types": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", - "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@borewit/text-codec": "^0.2.1", - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -6306,19 +5069,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", @@ -6350,21 +5100,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/typed-inject": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/typed-inject/-/typed-inject-5.0.0.tgz", @@ -6430,30 +5165,6 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/uint8array-extras": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", - "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, "node_modules/underscore": { "version": "1.13.8", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", @@ -6531,13 +5242,6 @@ "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -6584,28 +5288,6 @@ "node": ">= 8" } }, - "node_modules/which-typed-array": { - "version": "1.1.22", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", - "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -6661,26 +5343,6 @@ } } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/xz-decompress": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/xz-decompress/-/xz-decompress-0.2.3.tgz", - "integrity": "sha512-O8v6HG8T0PrKBcpyWA13GkSYWFvncwzuzcLx5A7++l3HsE3atmoetXjIxrZ/JV/nbvSZ7WS4+3XvREZuVn+rEA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -6757,19 +5419,6 @@ "node": ">=12" } }, - "node_modules/yauzl": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", - "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index edee83f4c..f1bf7e7a1 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,6 @@ "js-yaml": "^4.3.1", "mutation-testing-metrics": "^3.7.3", "re2js": "^2.8.6", - "shellcheck": "^4.1.0", "typescript": "^6.0.3", "typescript-eslint": "^8.60.0" }, diff --git a/scripts/lib/shellcheck-fetch.cjs b/scripts/lib/shellcheck-fetch.cjs new file mode 100644 index 000000000..806eae10f --- /dev/null +++ b/scripts/lib/shellcheck-fetch.cjs @@ -0,0 +1,247 @@ +'use strict'; + +/** + * shellcheck-fetch.cjs + * + * Dependency-free replacement for the `shellcheck` npm package (removed in + * #4120 — its extraction step pulled in `decompress@4.2.1`, which carries an + * unpatched CRITICAL zip-slip vulnerability, GHSA-mp2f-45pm-3cg9, CVSS 9.1, + * plus two moderate findings, with no patched version available upstream). + * + * This module fetches a PINNED koalaman/shellcheck release directly from + * GitHub releases (never "latest" — see SHELLCHECK_VERSION below), extracts + * the single `shellcheck` binary from the release's `.tar.gz` asset using + * only Node's built-in `https`/`zlib` modules plus a small hand-written tar + * reader (no third-party archive library), and caches the extracted binary + * for reuse across runs. + * + * Zip-slip defense: unlike `decompress`, which wrote extracted files using + * PATHS TAKEN FROM THE ARCHIVE (the exact defect class in GHSA-mp2f-45pm- + * 3cg9 — a malicious archive entry named e.g. `../../etc/passwd` gets + * written there verbatim), this reader NEVER uses an archive-supplied name + * as a filesystem path. `extractFileFromTar` only ever returns the matched + * entry's raw byte content; the caller (`resolveShellcheckBin`) writes those + * bytes to a path it constructs itself (`/shellcheck`), and the + * archive's own name field is used only for a string-equality/suffix CHECK + * (`name === targetName || name.endsWith('/' + targetName)`), never + * interpolated into a path passed to `fs.writeFileSync`/`fs.mkdirSync`/etc. + */ + +const fs = require('node:fs'); +const path = require('node:path'); +const zlib = require('node:zlib'); +const https = require('node:https'); +const { ExitError } = require('./cli-exit.cjs'); + +// Pinned explicitly — verified against `koalaman/shellcheck`'s GitHub +// releases API as the current latest tag at the time this was written +// (2026-08-31). Never resolved dynamically ("latest") — a moving target +// would make this lint's exact ShellCheck version, and therefore its exact +// finding set against scripts/lint-workflow-shellcheck-baseline.json, +// non-reproducible across runs/machines/CI. +const SHELLCHECK_VERSION = 'v0.11.0'; + +const ROOT = path.join(__dirname, '..', '..'); +const CACHE_DIR = path.join(ROOT, 'node_modules', '.cache', 'shellcheck', SHELLCHECK_VERSION); +const CACHED_BIN_PATH = path.join(CACHE_DIR, 'shellcheck'); + +// process.arch -> the arch token ShellCheck's release asset names use. +// Only the two architectures that actually matter for this repo (per +// .github/workflows/test.yml: the lint-tests job that runs this script only +// runs on ubuntu-latest, which is x86_64; and Apple Silicon dev machines are +// aarch64) are supported — anything else fails with a clear error rather +// than guessing. +const ARCH_MAP = { x64: 'x86_64', arm64: 'aarch64' }; + +const MAX_REDIRECTS = 5; + +// Bounds each individual HTTP hop (the initial request AND every redirect +// hop get their own fresh 30s budget, rather than one shared budget across +// the whole redirect chain) — a stalled connection on any single hop is +// caught in a bounded time, mirroring lint-workflow-shellcheck.cjs's own +// SHELLCHECK_TIMEOUT_MS bound on the ShellCheck subprocess. A bare `timeout` +// option on the request does NOT abort it by itself — Node only emits a +// 'timeout' event, which must be handled by destroying the request (see the +// `req.on('timeout', ...)` below). +const DOWNLOAD_TIMEOUT_MS = 30_000; + +/** + * Issue one real HTTPS request. Exists as its own function purely so tests + * can inject a fake in its place (see `httpsGetFollowingRedirects`'s + * `requestFn` parameter) — production callers never pass an override, so + * the real download path always uses this exact implementation. + */ +function defaultRequestFn(url, options, callback) { + return https.get(url, options, callback); +} + +/** + * GET `url` following HTTP redirects manually — `https.get` does NOT follow + * redirects automatically, and GitHub release asset URLs redirect through + * `objects.githubusercontent.com`. Resolves with the full response body as a + * Buffer once a 200 response is received. + * + * `requestFn` defaults to a real `https.get`-based transport + * (`defaultRequestFn`) and is only ever overridden in tests, so calling this + * with zero/one arg from `resolveShellcheckBin` is unchanged behavior. + */ +function httpsGetFollowingRedirects(url, redirectsLeft = MAX_REDIRECTS, requestFn = defaultRequestFn) { + return new Promise((resolve, reject) => { + const req = requestFn( + url, + { headers: { 'User-Agent': 'gsd-core-shellcheck-fetch' }, timeout: DOWNLOAD_TIMEOUT_MS }, + (res) => { + const status = res.statusCode || 0; + if (status >= 300 && status < 400 && res.headers.location) { + res.resume(); // drain so the socket can be reused/closed + if (redirectsLeft <= 0) { + reject(new Error(`too many redirects fetching ${url}`)); + return; + } + const next = new URL(res.headers.location, url).toString(); + httpsGetFollowingRedirects(next, redirectsLeft - 1, requestFn).then(resolve, reject); + return; + } + if (status !== 200) { + res.resume(); + reject(new Error(`unexpected HTTP ${status} fetching ${url}`)); + return; + } + const chunks = []; + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => resolve(Buffer.concat(chunks))); + res.on('error', reject); + }, + ); + req.on('error', reject); + // `timeout` in the options above only ARMS a timer — Node emits a + // 'timeout' event on the request but does not abort it. Without this + // handler the request (and this Promise) would hang indefinitely past + // the configured bound on a stalled connection. + req.on('timeout', () => { + req.destroy(new Error(`timed out after ${DOWNLOAD_TIMEOUT_MS}ms fetching ${url}`)); + }); + }); +} + +/** + * Find the entry named (or path-ending-in) `targetName` inside a raw + * (already gunzipped) POSIX tar byte stream and return its content as a + * Buffer, or `null` if not found. + * + * Tar format: a sequence of 512-byte headers — name at offset 0/length 100, + * size at offset 124/length 12 (octal ASCII), typeflag at offset 156 — each + * followed by that many content bytes, padded up to the next 512-byte + * boundary, terminated by an all-zero 512-byte block. This deliberately + * implements only enough to locate ONE known entry name (no general + * multi-file extraction, no symlink handling, no GNU long-name `@LongLink` + * entries — ShellCheck's own release tarballs never need them) — see this + * module's header comment for why the entry's NAME is never used as a + * filesystem path. + */ +function extractFileFromTar(buffer, targetName) { + let offset = 0; + while (offset + 512 <= buffer.length) { + const header = buffer.subarray(offset, offset + 512); + if (header.every((b) => b === 0)) break; // end-of-archive marker + const name = header.subarray(0, 100).toString('utf8').replace(/\0.*$/, ''); + const sizeRaw = header.subarray(124, 136).toString('utf8').replace(/\0.*$/, '').trim(); + const size = sizeRaw === '' ? 0 : parseInt(sizeRaw, 8); + const typeflag = String.fromCharCode(header[156]); + const dataStart = offset + 512; + const isRegularFile = typeflag === '0' || typeflag === '\0'; + if (isRegularFile && (name === targetName || name.endsWith(`/${targetName}`))) { + return buffer.subarray(dataStart, dataStart + size); + } + const contentBlocks = Math.ceil(size / 512); + offset = dataStart + contentBlocks * 512; + } + return null; +} + +/** + * Resolve the local path to a working, executable `shellcheck` binary, + * downloading and caching the pinned release on first use. Subsequent calls + * (same version) reuse the cached binary with no network activity — mirrors + * the `fs.accessSync(bin, F_OK | X_OK)` cache-check pattern already used by + * this script's own `runShellcheck`. + */ +async function resolveShellcheckBin() { + try { + fs.accessSync(CACHED_BIN_PATH, fs.constants.F_OK | fs.constants.X_OK); + return CACHED_BIN_PATH; + } catch { + // not cached yet — fall through to download + } + + if (process.platform === 'win32') { + throw new ExitError( + 1, + 'lint-workflow-shellcheck: automatic ShellCheck download is not supported on Windows yet ' + + '(this lint only ever runs in the ubuntu-latest lint-tests CI job — see .github/workflows/test.yml — ' + + 'so this is an honest platform gap, not expected to be hit in CI).', + ); + } + const platform = process.platform === 'darwin' || process.platform === 'linux' ? process.platform : null; + if (!platform) { + throw new ExitError( + 1, + `lint-workflow-shellcheck: unsupported platform '${process.platform}' for ShellCheck auto-download ` + + `(supported: linux, darwin).`, + ); + } + const arch = ARCH_MAP[process.arch]; + if (!arch) { + throw new ExitError( + 1, + `lint-workflow-shellcheck: unsupported architecture '${process.arch}' for ShellCheck auto-download ` + + `(supported: x86_64 [node arch 'x64'], aarch64 [node arch 'arm64']).`, + ); + } + + const assetName = `shellcheck-${SHELLCHECK_VERSION}.${platform}.${arch}.tar.gz`; + const url = `https://github.com/koalaman/shellcheck/releases/download/${SHELLCHECK_VERSION}/${assetName}`; + + let gz; + try { + gz = await httpsGetFollowingRedirects(url); + } catch (e) { + throw new ExitError(1, `lint-workflow-shellcheck: failed to download ShellCheck (${url}): ${e.message}`); + } + + let tarBuf; + try { + tarBuf = zlib.gunzipSync(gz); + } catch (e) { + throw new ExitError(1, `lint-workflow-shellcheck: failed to gunzip downloaded ShellCheck archive: ${e.message}`); + } + + const entry = extractFileFromTar(tarBuf, 'shellcheck'); + if (!entry) { + throw new ExitError( + 1, + `lint-workflow-shellcheck: could not find a 'shellcheck' entry inside downloaded archive ${assetName}`, + ); + } + + fs.mkdirSync(CACHE_DIR, { recursive: true }); + // Write to a per-process temp path and rename into place — avoids any + // other concurrent invocation observing (and trying to execute) a + // partially-written binary at the real cache path. + const tmpPath = path.join(CACHE_DIR, `.shellcheck.tmp-${process.pid}`); + fs.writeFileSync(tmpPath, entry); + fs.chmodSync(tmpPath, 0o755); + fs.renameSync(tmpPath, CACHED_BIN_PATH); + + return CACHED_BIN_PATH; +} + +module.exports = { + SHELLCHECK_VERSION, + CACHED_BIN_PATH, + MAX_REDIRECTS, + DOWNLOAD_TIMEOUT_MS, + extractFileFromTar, + httpsGetFollowingRedirects, + resolveShellcheckBin, +}; diff --git a/scripts/lint-workflow-shellcheck.cjs b/scripts/lint-workflow-shellcheck.cjs index 6f1654742..d1864b1da 100644 --- a/scripts/lint-workflow-shellcheck.cjs +++ b/scripts/lint-workflow-shellcheck.cjs @@ -13,14 +13,15 @@ * bash) from landing undetected a second time (it already landed 4 times in * this repo's workflow templates before #4109's fix). * - * ShellCheck source: the `shellcheck` npm package (gunar/shellcheck), a thin - * wrapper that downloads the official koalaman/shellcheck binary on first - * use and caches it under node_modules/shellcheck/bin/. Chosen over the - * alternatives surveyed (node-shellcheck: ~5 weekly downloads, last - * published 2022; shellcheck-binaries: ~280 weekly downloads, last - * published 2022) because it has ~80k weekly downloads and is the - * only actively-maintained wrapper — it downloads the CURRENT upstream - * ShellCheck release rather than vendoring a stale binary snapshot. + * ShellCheck source: scripts/lib/shellcheck-fetch.cjs, a small dependency- + * free downloader that fetches a PINNED koalaman/shellcheck release directly + * from GitHub releases and caches the extracted binary under + * node_modules/.cache/shellcheck//. This replaces the `shellcheck` + * npm package (gunar/shellcheck) originally used here (#4109) — removed in + * #4120 because its extraction dependency, `decompress@4.2.1`, carries an + * unpatched CRITICAL zip-slip vulnerability (GHSA-mp2f-45pm-3cg9, CVSS 9.1) + * with no patched version available upstream. See shellcheck-fetch.cjs's own + * header comment for the extraction implementation and its zip-slip defense. * * Extraction: reuses scanFencedBlocks from markdown-sectionizer.cts (the * canonical fence-scanning engine — see tests/review-plan-coverage-manifest @@ -112,23 +113,17 @@ const os = require('node:os'); const path = require('node:path'); const childProcess = require('node:child_process'); const { ExitError, runMain } = require('./lib/cli-exit.cjs'); +const { resolveShellcheckBin } = require('./lib/shellcheck-fetch.cjs'); // Hard bound on the ShellCheck binary's run time, matching this repo's // npm-subprocess timeout convention (5-30s git, 60s npm — same "external -// process that could hang" hazard class). See runShellcheck's comment for -// why this cannot be applied via the `shellcheck` npm package's own API. +// process that could hang" hazard class). Applied directly to runShellcheck's +// own spawnSync call below. const SHELLCHECK_TIMEOUT_MS = 60_000; const ROOT = path.join(__dirname, '..'); const WORKFLOWS_DIR = path.join(ROOT, 'gsd-core', 'workflows'); const SECTIONIZER_PATH = path.join(ROOT, 'gsd-core', 'bin', 'lib', 'markdown-sectionizer.cjs'); -const SHELLCHECK_BIN_MODULE = path.join(ROOT, 'node_modules', 'shellcheck', 'build', 'index.js'); -// The top-level SHELLCHECK_BIN_MODULE barrel (build/index.js) does NOT -// re-export `configs/index.js` (verified: `export *`-ing helpers/logger/ -// utils/shellcheck.js only — no configs), so `config` (which carries the -// resolved binary path used by runShellcheck's own spawnSync call, see -// below) has to be imported from its own submodule directly. -const SHELLCHECK_CONFIG_MODULE = path.join(ROOT, 'node_modules', 'shellcheck', 'build', 'configs', 'index.js'); const BASELINE_PATH = path.join(__dirname, 'lint-workflow-shellcheck-baseline.json'); // Codes excluded for structural reasons documented in the module header above. @@ -443,58 +438,25 @@ function loadSectionizer() { } } -/** Load the `shellcheck` npm package's programmatic API — its own `shellcheck()` - * function transparently downloads the real binary to node_modules/shellcheck/ - * bin/shellcheck (caching it there) on first use if it is not already present. */ -async function loadShellcheckModule() { - try { - const mod = await import(SHELLCHECK_BIN_MODULE); - // See SHELLCHECK_CONFIG_MODULE's comment above — `config` is not part of - // the top-level barrel's exports, so it is imported separately and - // attached here for runShellcheck's direct spawnSync call to consume. - const { config } = await import(SHELLCHECK_CONFIG_MODULE); - return { ...mod, config }; - } catch (e) { - throw new ExitError( - 1, - `lint-workflow-shellcheck: cannot load the 'shellcheck' npm package at ` + - `${path.relative(ROOT, SHELLCHECK_BIN_MODULE)} — run 'npm install' first (${e.message})`, - ); - } -} - /** * Run ShellCheck (json1 output) over every staged temp file in one invocation, * bounded by SHELLCHECK_TIMEOUT_MS. * - * The `shellcheck` npm package's own `shellcheck()` function does NOT accept a - * `timeout` — its `ShellCheckArgs` type is `{bin, args, stdio, token}` only - * (verified against node_modules/shellcheck/build/shellcheck.d.ts and .js), - * and internally it hardcodes `child_process.spawnSync(opts.bin, opts.args, { - * stdio: opts.stdio })` with no pass-through for extra spawnSync options. - * Wrapping the call in `Promise.race` against a timer would not help either: - * spawnSync is synchronous and blocks the event loop for its whole duration, - * so a timer callback racing it can never fire before it returns (or hangs). - * Instead, this reimplements the same binary-resolve-and-download step the - * wrapper performs (via the package's own exported `config`/`download`), then - * invokes `child_process.spawnSync` directly with a native `timeout` so a - * hung ShellCheck binary is killed (Node sets `result.error.code === - * 'ETIMEDOUT'` and `result.signal` on expiry) rather than hanging this lint — - * and, transitively, CI — indefinitely. + * `bin` is resolved by the caller via scripts/lib/shellcheck-fetch.cjs's + * `resolveShellcheckBin()` (downloading and caching the pinned release on + * first use, per that module's own header comment). This invokes + * `child_process.spawnSync` directly with a native `timeout` so a hung + * ShellCheck binary is killed (Node sets `result.error.code === 'ETIMEDOUT'` + * and `result.signal` on expiry) rather than hanging this lint — and, + * transitively, CI — indefinitely. */ -async function runShellcheck(mod, filePaths) { +function runShellcheck(bin, filePaths) { const args = [ '--shell=bash', '--format=json1', `--exclude=${EXCLUDED_CODES.join(',')}`, ...filePaths, ]; - const bin = mod.config.bin; - try { - fs.accessSync(bin, fs.constants.F_OK | fs.constants.X_OK); - } catch { - await mod.download({ destination: bin, token: process.env.GITHUB_TOKEN }); - } const result = childProcess.spawnSync(bin, args, { stdio: 'pipe', timeout: SHELLCHECK_TIMEOUT_MS }); if (result.error) { const timedOut = result.error.code === 'ETIMEDOUT'; @@ -560,7 +522,7 @@ async function main() { } const structuralFailed = structuralFindings.length > 0; - const shellcheckModule = await loadShellcheckModule(); + const shellcheckBin = await resolveShellcheckBin(); const stageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-workflow-shellcheck-')); try { @@ -573,7 +535,7 @@ async function main() { byPath.set(scriptPath, block); }); - const findings = await runShellcheck(shellcheckModule, stagedPaths); + const findings = runShellcheck(shellcheckBin, stagedPaths); if (findings.length === 0) { process.stdout.write( diff --git a/tests/fixtures/install-tree/antigravity.json b/tests/fixtures/install-tree/antigravity.json index 63af1ec7d..b663a59a0 100644 --- a/tests/fixtures/install-tree/antigravity.json +++ b/tests/fixtures/install-tree/antigravity.json @@ -439,6 +439,7 @@ "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs", "skills/gsd-add-tests/SKILL.md", "skills/gsd-ai-integration-phase/SKILL.md", "skills/gsd-audit-fix/SKILL.md", diff --git a/tests/fixtures/install-tree/augment.json b/tests/fixtures/install-tree/augment.json index 897c9a418..ef4505944 100644 --- a/tests/fixtures/install-tree/augment.json +++ b/tests/fixtures/install-tree/augment.json @@ -509,6 +509,7 @@ "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs", "skills/gsd-ns-context/SKILL.md", "skills/gsd-ns-context/skills/docs-update/SKILL.md", "skills/gsd-ns-context/skills/extract-learnings/SKILL.md", diff --git a/tests/fixtures/install-tree/claude-local.json b/tests/fixtures/install-tree/claude-local.json index 692eb849f..cb3f21ba1 100644 --- a/tests/fixtures/install-tree/claude-local.json +++ b/tests/fixtures/install-tree/claude-local.json @@ -508,5 +508,6 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", - "scripts/lib/ndjson-reporter.cjs" + "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs" ] diff --git a/tests/fixtures/install-tree/claude.json b/tests/fixtures/install-tree/claude.json index 49c6defc8..bef6bc63c 100644 --- a/tests/fixtures/install-tree/claude.json +++ b/tests/fixtures/install-tree/claude.json @@ -438,6 +438,7 @@ "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs", "skills/gsd-add-tests/SKILL.md", "skills/gsd-ai-integration-phase/SKILL.md", "skills/gsd-audit-fix/SKILL.md", diff --git a/tests/fixtures/install-tree/cline.json b/tests/fixtures/install-tree/cline.json index 2a1b840e5..bc109cec7 100644 --- a/tests/fixtures/install-tree/cline.json +++ b/tests/fixtures/install-tree/cline.json @@ -401,6 +401,7 @@ "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs", "skills/gsd-ns-context/SKILL.md", "skills/gsd-ns-context/skills/docs-update/SKILL.md", "skills/gsd-ns-context/skills/extract-learnings/SKILL.md", diff --git a/tests/fixtures/install-tree/codebuddy.json b/tests/fixtures/install-tree/codebuddy.json index e9a2563c2..b739587f9 100644 --- a/tests/fixtures/install-tree/codebuddy.json +++ b/tests/fixtures/install-tree/codebuddy.json @@ -509,6 +509,7 @@ "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs", "skills/gsd-add-tests/SKILL.md", "skills/gsd-ai-integration-phase/SKILL.md", "skills/gsd-audit-fix/SKILL.md", diff --git a/tests/fixtures/install-tree/codex.json b/tests/fixtures/install-tree/codex.json index a5a8cb429..c29bcb6b8 100644 --- a/tests/fixtures/install-tree/codex.json +++ b/tests/fixtures/install-tree/codex.json @@ -439,5 +439,6 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", - "scripts/lib/ndjson-reporter.cjs" + "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs" ] diff --git a/tests/fixtures/install-tree/copilot.json b/tests/fixtures/install-tree/copilot.json index efb402242..573161092 100644 --- a/tests/fixtures/install-tree/copilot.json +++ b/tests/fixtures/install-tree/copilot.json @@ -401,6 +401,7 @@ "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs", "skills/gsd-add-tests/SKILL.md", "skills/gsd-ai-integration-phase/SKILL.md", "skills/gsd-audit-fix/SKILL.md", diff --git a/tests/fixtures/install-tree/cursor.json b/tests/fixtures/install-tree/cursor.json index c4a1828a4..74eb079d6 100644 --- a/tests/fixtures/install-tree/cursor.json +++ b/tests/fixtures/install-tree/cursor.json @@ -412,6 +412,7 @@ "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs", "skills/gsd-add-tests/SKILL.md", "skills/gsd-ai-integration-phase/SKILL.md", "skills/gsd-audit-fix/SKILL.md", diff --git a/tests/fixtures/install-tree/hermes.json b/tests/fixtures/install-tree/hermes.json index c171c535e..c7a77a8c6 100644 --- a/tests/fixtures/install-tree/hermes.json +++ b/tests/fixtures/install-tree/hermes.json @@ -438,6 +438,7 @@ "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs", "skills/gsd/DESCRIPTION.md", "skills/gsd/gsd-ns-context/SKILL.md", "skills/gsd/gsd-ns-context/skills/docs-update/SKILL.md", diff --git a/tests/fixtures/install-tree/kilo.json b/tests/fixtures/install-tree/kilo.json index d8297fdd4..b2556d09e 100644 --- a/tests/fixtures/install-tree/kilo.json +++ b/tests/fixtures/install-tree/kilo.json @@ -512,6 +512,7 @@ "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs", "skills/gsd-add-tests/SKILL.md", "skills/gsd-ai-integration-phase/SKILL.md", "skills/gsd-audit-fix/SKILL.md", diff --git a/tests/fixtures/install-tree/kimi-code.json b/tests/fixtures/install-tree/kimi-code.json index cf9d9b7a8..055a27d1e 100644 --- a/tests/fixtures/install-tree/kimi-code.json +++ b/tests/fixtures/install-tree/kimi-code.json @@ -439,6 +439,7 @@ "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs", "skills/gsd-add-tests/SKILL.md", "skills/gsd-ai-integration-phase/SKILL.md", "skills/gsd-audit-fix/SKILL.md", diff --git a/tests/fixtures/install-tree/kimi.json b/tests/fixtures/install-tree/kimi.json index 831b8d805..e2c745eb8 100644 --- a/tests/fixtures/install-tree/kimi.json +++ b/tests/fixtures/install-tree/kimi.json @@ -436,6 +436,7 @@ "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs", "skills/gsd-add-tests/SKILL.md", "skills/gsd-ai-integration-phase/SKILL.md", "skills/gsd-audit-fix/SKILL.md", diff --git a/tests/fixtures/install-tree/opencode.json b/tests/fixtures/install-tree/opencode.json index aa34580a4..a7f6465e8 100644 --- a/tests/fixtures/install-tree/opencode.json +++ b/tests/fixtures/install-tree/opencode.json @@ -512,6 +512,7 @@ "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs", "skills/gsd-add-tests/SKILL.md", "skills/gsd-ai-integration-phase/SKILL.md", "skills/gsd-audit-fix/SKILL.md", diff --git a/tests/fixtures/install-tree/pi.json b/tests/fixtures/install-tree/pi.json index a7fba9d53..0cecdd072 100644 --- a/tests/fixtures/install-tree/pi.json +++ b/tests/fixtures/install-tree/pi.json @@ -404,5 +404,6 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", - "scripts/lib/ndjson-reporter.cjs" + "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs" ] diff --git a/tests/fixtures/install-tree/qwen.json b/tests/fixtures/install-tree/qwen.json index d1a9a9edb..639d2052d 100644 --- a/tests/fixtures/install-tree/qwen.json +++ b/tests/fixtures/install-tree/qwen.json @@ -438,6 +438,7 @@ "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs", "skills/gsd-ns-context/SKILL.md", "skills/gsd-ns-context/skills/docs-update/SKILL.md", "skills/gsd-ns-context/skills/extract-learnings/SKILL.md", diff --git a/tests/fixtures/install-tree/trae.json b/tests/fixtures/install-tree/trae.json index 046e15e9b..612bc7979 100644 --- a/tests/fixtures/install-tree/trae.json +++ b/tests/fixtures/install-tree/trae.json @@ -399,6 +399,7 @@ "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs", "skills/gsd-ns-context/SKILL.md", "skills/gsd-ns-context/skills/docs-update/SKILL.md", "skills/gsd-ns-context/skills/extract-learnings/SKILL.md", diff --git a/tests/fixtures/install-tree/windsurf.json b/tests/fixtures/install-tree/windsurf.json index a2faa482b..e640990c1 100644 --- a/tests/fixtures/install-tree/windsurf.json +++ b/tests/fixtures/install-tree/windsurf.json @@ -401,5 +401,6 @@ "scripts/lib/cli-exit.cjs", "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", - "scripts/lib/ndjson-reporter.cjs" + "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs" ] diff --git a/tests/fixtures/install-tree/zcode.json b/tests/fixtures/install-tree/zcode.json index 7708f7ca1..f7ecec19a 100644 --- a/tests/fixtures/install-tree/zcode.json +++ b/tests/fixtures/install-tree/zcode.json @@ -470,6 +470,7 @@ "scripts/lib/drift-scan.cjs", "scripts/lib/exit-code-registry.cjs", "scripts/lib/ndjson-reporter.cjs", + "scripts/lib/shellcheck-fetch.cjs", "skills/gsd-ns-context/SKILL.md", "skills/gsd-ns-context/skills/docs-update/SKILL.md", "skills/gsd-ns-context/skills/extract-learnings/SKILL.md", diff --git a/tests/lint-workflow-shellcheck-fetch.test.cjs b/tests/lint-workflow-shellcheck-fetch.test.cjs new file mode 100644 index 000000000..796412560 --- /dev/null +++ b/tests/lint-workflow-shellcheck-fetch.test.cjs @@ -0,0 +1,348 @@ +'use strict'; + +/** + * lint-workflow-shellcheck-fetch.test.cjs — unit + property coverage for the + * hand-rolled tar reader exported by scripts/lib/shellcheck-fetch.cjs + * (#4120: `decompress` npm package removal, GHSA-mp2f-45pm-3cg9 zip-slip). + * + * Per this repo's CLAUDE.md: "Parsers, budget limits, and bijective + * contracts must include at least one fast-check (`fc`) property test." + * `extractFileFromTar` is a hand-written tar-format parser with a + * security-relevant property (an archive-supplied entry name must never + * influence anything beyond a string-equality/suffix lookup — see + * scripts/lib/shellcheck-fetch.cjs's header comment on the zip-slip defense + * this module replaces), so the coverage below is a binding gate, not + * optional polish. + * + * Test matrix: .gsd/bug/fix-4120-shellcheck-decompress-cve/50-test-matrix.md + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); +const fs = require('node:fs'); +const { EventEmitter } = require('node:events'); +const fc = require('./helpers/fast-check-setup.cjs'); + +const { + extractFileFromTar, + httpsGetFollowingRedirects, + MAX_REDIRECTS, + DOWNLOAD_TIMEOUT_MS, +} = require(path.join(__dirname, '..', 'scripts', 'lib', 'shellcheck-fetch.cjs')); + +// --- test-only fixture builders -------------------------------------------- +// +// Construct synthetic POSIX tar buffers matching exactly what +// extractFileFromTar's own header comment (scripts/lib/shellcheck-fetch.cjs) +// documents: name at offset 0/length 100, size as octal ASCII at offset +// 124/length 12, typeflag at offset 156, content padded to a 512-byte +// boundary, terminated by an all-zero 512-byte block. These builders are +// test-only fixture plumbing — deliberately NOT exported from the +// production module. + +/** Build a single 512-byte header + padded-content tar entry. */ +function buildTarEntry(name, content) { + const contentBuf = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf8'); + const header = Buffer.alloc(512); // zero-filled: null-pads name/size fields for free + header.write(name, 0, 100, 'utf8'); + const sizeOctal = `${contentBuf.length.toString(8).padStart(11, '0')}\0`; // 12 bytes total + header.write(sizeOctal, 124, 12, 'utf8'); + header[156] = '0'.charCodeAt(0); // typeflag '0' == regular file + const paddedLen = Math.ceil(contentBuf.length / 512) * 512; + const contentBlock = Buffer.alloc(paddedLen); + contentBuf.copy(contentBlock, 0); + return Buffer.concat([header, contentBlock]); +} + +/** Build a full tar byte stream from `[name, content]` pairs, terminated by + * the mandatory all-zero 512-byte end-of-archive block. */ +function buildTar(entries) { + const parts = entries.map(([name, content]) => buildTarEntry(name, content)); + parts.push(Buffer.alloc(512)); // terminating zero block + return Buffer.concat(parts); +} + +describe('extractFileFromTar', () => { + test('finds and returns the exact bytes for an entry matching the target name exactly', () => { + const tar = buildTar([['shellcheck', 'binary-bytes-here']]); + const result = extractFileFromTar(tar, 'shellcheck'); + assert.equal(result.toString('utf8'), 'binary-bytes-here'); + }); + + test('finds a path-prefixed entry (name.endsWith("/" + targetName)) per the documented match rule', () => { + const tar = buildTar([['somedir/shellcheck', 'nested-binary']]); + const result = extractFileFromTar(tar, 'shellcheck'); + assert.equal(result.toString('utf8'), 'nested-binary'); + }); + + test('returns null when no entry matches', () => { + const tar = buildTar([ + ['README.md', 'docs'], + ['LICENSE', 'license text'], + ]); + assert.equal(extractFileFromTar(tar, 'shellcheck'), null); + }); + + test('skips over non-matching entries of varying sizes (exact 512-multiples and padding-requiring sizes) to reach a later match', () => { + const tar = buildTar([ + ['a-file', Buffer.alloc(512, 0x41)], // exact block multiple, no padding needed + ['b-file', Buffer.alloc(1024, 0x42)], // exact 2-block multiple + ['c-file', Buffer.alloc(1, 0x43)], // 1 byte, needs 511 bytes of padding + ['d-file', Buffer.alloc(513, 0x44)], // 513 bytes, needs 511 bytes of padding + ['shellcheck', 'the-real-binary'], + ]); + const result = extractFileFromTar(tar, 'shellcheck'); + assert.equal(result.toString('utf8'), 'the-real-binary'); + }); + + test('stops at the terminating all-zero block rather than reading garbage (or a later match) past it', () => { + // Deliberately bypass buildTar's automatic single terminator: place a + // real terminating zero block in the MIDDLE of the buffer, with a + // matching entry planted after it. If the reader kept scanning past the + // terminator, this would (incorrectly) find and return the post- + // terminator entry instead of null. + const tar = Buffer.concat([ + buildTarEntry('unrelated', 'x'), + Buffer.alloc(512), // terminating zero block, NOT at end of buffer + buildTarEntry('shellcheck', 'should-never-be-reached'), + ]); + assert.equal(extractFileFromTar(tar, 'shellcheck'), null); + }); + + // --- security property: the archive-supplied name is data, never a path --- + // + // The actual zip-slip defense is architectural: resolveShellcheckBin (in + // scripts/lib/shellcheck-fetch.cjs) never passes the matched entry's name + // to fs.writeFileSync/mkdirSync/etc — it only ever writes to a path it + // constructs itself. This test does NOT prove that architectural fact by + // itself; it pins the narrower, directly-testable behavioral contract of + // extractFileFromTar in isolation: a traversal-style or otherwise + // malicious name is matched by plain string equality/suffix like any other + // name, and the function never touches the filesystem while doing so. + test('security: traversal-style names are matched by plain string equality/suffix and the function never touches the filesystem', () => { + const fsMethods = ['writeFileSync', 'mkdirSync', 'renameSync', 'chmodSync', 'accessSync', 'readFileSync', 'unlinkSync']; + const originals = {}; + for (const m of fsMethods) { + originals[m] = fs[m]; + fs[m] = () => { + throw new Error(`extractFileFromTar unexpectedly invoked fs.${m} — the archive name must never reach the filesystem`); + }; + } + try { + // A traversal-style name that does NOT satisfy the match rule (does + // not equal, and does not end with "/shellcheck") — no match, no fs + // activity, no throw. + const noMatchTar = buildTar([['../../../etc/passwd', 'pwned-content']]); + assert.equal(extractFileFromTar(noMatchTar, 'shellcheck'), null); + + // A traversal-prefixed name that DOES satisfy the documented suffix + // rule (ends with "/shellcheck") matches exactly like any other + // path-prefixed name — the traversal segments are inert string data, + // never resolved or touched as a path by this function. + const matchTar = buildTar([['../../shellcheck', 'traversal-prefixed-but-matches']]); + const result = extractFileFromTar(matchTar, 'shellcheck'); + assert.equal(result.toString('utf8'), 'traversal-prefixed-but-matches'); + } finally { + for (const m of fsMethods) fs[m] = originals[m]; + } + }); + + // --- fast-check property test (CLAUDE.md-mandated for parsers) --- + // + // Property: for a randomly generated set of tar entries — some named + // exactly the target name, most not, with content lengths spanning the + // 512-byte block boundary at several points (0, 1, 511, 512, 513, 1023, + // 1024 bytes) — extractFileFromTar returns exactly the bytes of the FIRST + // entry whose name equals the target name, or null if none does. This is + // the round-trip check that would catch an off-by-one in the + // padding/block-alignment arithmetic (contentBlocks = ceil(size / 512)) + // across a wide range of sizes and entry-count/ordering combinations, + // which the hand-picked boundary cases above only sample. + test('property: finds the first entry whose name matches the target and returns its exact original bytes, else null', () => { + const targetName = 'shellcheck'; + // Never collides with targetName and never contains "/", so it can + // never accidentally satisfy either match branch. + const otherNameArb = fc.stringMatching(/^[a-z][a-z0-9_-]{0,10}$/).map((s) => `not-${s}`); + const nameArb = fc.oneof({ arbitrary: otherNameArb, weight: 3 }, { arbitrary: fc.constant(targetName), weight: 1 }); + const sizeArb = fc.constantFrom(0, 1, 511, 512, 513, 1023, 1024); + const entryArb = fc + .tuple(nameArb, sizeArb) + .chain(([name, size]) => + fc.uint8Array({ minLength: size, maxLength: size }).map((bytes) => ({ name, content: Buffer.from(bytes) })), + ); + + fc.assert( + fc.property(fc.array(entryArb, { maxLength: 8 }), (entries) => { + const tar = buildTar(entries.map((e) => [e.name, e.content])); + const result = extractFileFromTar(tar, targetName); + const firstMatchIdx = entries.findIndex((e) => e.name === targetName); + if (firstMatchIdx === -1) { + assert.equal(result, null); + } else { + assert.notEqual(result, null); + assert.equal(Buffer.compare(result, entries[firstMatchIdx].content), 0); + } + }), + ); + }); +}); + +describe('httpsGetFollowingRedirects', () => { + // --- test-only fake transport --------------------------------------- + // + // httpsGetFollowingRedirects's third parameter (`requestFn`, defaulting + // to a real https.get-based transport) exists purely so these tests can + // script a deterministic, offline sequence of redirect/200/error + // responses instead of hitting the real network. Each call below + // consumes exactly one queued `step` and invokes the callback with a + // fake response (a plain EventEmitter carrying statusCode/headers, plus + // a no-op resume() and, for step.body, async data/end emission). + + /** Build a fake response EventEmitter matching what real `http.IncomingMessage` exposes here: statusCode, headers, resume(), and (for 200s) data/end events. */ + function makeFakeResponse(step) { + const res = new EventEmitter(); + res.statusCode = step.status; + res.headers = step.headers || {}; + res.resume = () => {}; + if (step.body !== undefined) { + // Emit asynchronously so the production code's `res.on(...)` calls + // (registered synchronously right after this response is handed to + // the callback) are attached before anything fires — matching real + // stream timing. + process.nextTick(() => { + res.emit('data', Buffer.isBuffer(step.body) ? step.body : Buffer.from(step.body)); + res.emit('end'); + }); + } + return res; + } + + /** + * Build a scriptable `requestFn` that hands back the next queued `step` + * (`{status, headers?, body?}`) on each call, in order — one call per + * HTTP hop (initial request + each redirect). Throws if called more + * times than steps were provided, so a test's step count doubles as an + * assertion on exactly how many hops the production code performs. + * `reqs` (if provided) collects every fake request object created, for + * tests that need to drive request-level events (e.g. 'timeout'). + */ + function makeFakeRequestFn(steps, reqs) { + let i = 0; + return function fakeRequestFn(url, options, callback) { + const req = new EventEmitter(); + req.destroy = (err) => { + if (err) req.emit('error', err); + }; + if (reqs) reqs.push(req); + const step = steps[i++]; + assert.ok(step, `fakeRequestFn called more times (call #${i}) than steps provided (${steps.length})`); + process.nextTick(() => callback(makeFakeResponse(step))); + return req; + }; + } + + test(`limit-1 (${MAX_REDIRECTS - 1} redirects, MAX_REDIRECTS=${MAX_REDIRECTS}): succeeds and returns the final 200 body`, async () => { + const steps = [ + ...Array.from({ length: MAX_REDIRECTS - 1 }, (_, n) => ({ + status: 302, + headers: { location: `https://example.invalid/hop-${n + 1}` }, + })), + { status: 200, body: 'final-bytes' }, + ]; + const result = await httpsGetFollowingRedirects( + 'https://example.invalid/start', + MAX_REDIRECTS, + makeFakeRequestFn(steps), + ); + assert.equal(result.toString('utf8'), 'final-bytes'); + }); + + test(`limit (exactly ${MAX_REDIRECTS} redirects, MAX_REDIRECTS=${MAX_REDIRECTS}): succeeds — a chain exactly at the limit is not off-by-one rejected`, async () => { + const steps = [ + ...Array.from({ length: MAX_REDIRECTS }, (_, n) => ({ + status: 302, + headers: { location: `https://example.invalid/hop-${n + 1}` }, + })), + { status: 200, body: 'final-bytes-at-limit' }, + ]; + const result = await httpsGetFollowingRedirects( + 'https://example.invalid/start', + MAX_REDIRECTS, + makeFakeRequestFn(steps), + ); + assert.equal(result.toString('utf8'), 'final-bytes-at-limit'); + }); + + test(`limit+1 (${MAX_REDIRECTS + 1} redirects, MAX_REDIRECTS=${MAX_REDIRECTS}): rejects with a clear "too many redirects" error`, async () => { + // No trailing 200 step: the (MAX_REDIRECTS + 1)th redirect is expected + // to be rejected before a further hop would ever be attempted. + const steps = Array.from({ length: MAX_REDIRECTS + 1 }, (_, n) => ({ + status: 302, + headers: { location: `https://example.invalid/hop-${n + 1}` }, + })); + await assert.rejects( + httpsGetFollowingRedirects('https://example.invalid/start', MAX_REDIRECTS, makeFakeRequestFn(steps)), + /too many redirects fetching/, + ); + }); + + test('a non-3xx, non-200 status (404) rejects with a clear error naming the status', async () => { + const steps = [{ status: 404 }]; + await assert.rejects( + httpsGetFollowingRedirects('https://example.invalid/asset', MAX_REDIRECTS, makeFakeRequestFn(steps)), + /unexpected HTTP 404 fetching https:\/\/example\.invalid\/asset/, + ); + }); + + test('a non-3xx, non-200 status (500) rejects with a clear error naming the status', async () => { + const steps = [{ status: 500 }]; + await assert.rejects( + httpsGetFollowingRedirects('https://example.invalid/asset', MAX_REDIRECTS, makeFakeRequestFn(steps)), + /unexpected HTTP 500 fetching https:\/\/example\.invalid\/asset/, + ); + }); + + test('a 3xx response with a MISSING location header does not redirect-loop — it falls through to the status!==200 rejection', async () => { + // Per the current code: the redirect branch is only entered when + // `status >= 300 && status < 400 && res.headers.location` — no + // location means that condition is false, so this falls straight + // through to the `status !== 200` check below it and rejects there, + // rather than looping or crashing on a missing `location`. + const steps = [{ status: 302, headers: {} }]; + await assert.rejects( + httpsGetFollowingRedirects('https://example.invalid/asset', MAX_REDIRECTS, makeFakeRequestFn(steps)), + /unexpected HTTP 302 fetching https:\/\/example\.invalid\/asset/, + ); + }); + + // --- Gap 1 coverage: the request-level timeout handler ----------------- + // + // Confirms the fix for the unbounded-download hazard: a request that + // never receives a response is rejected once its 'timeout' event fires, + // rather than hanging forever. The fake transport never calls back on + // its own — this test drives the timeout itself, so it runs instantly + // rather than waiting out the real DOWNLOAD_TIMEOUT_MS. + test('a request that times out (no response ever received) is destroyed and rejects with a diagnostic message', async () => { + const reqs = []; + const hangingRequestFn = (_url, _options, _callback) => { + const req = new EventEmitter(); + req.destroy = (err) => { + if (err) req.emit('error', err); + }; + reqs.push(req); + // Deliberately never invokes _callback — simulates a stalled + // connection that never produces a response. + return req; + }; + + const promise = httpsGetFollowingRedirects('https://example.invalid/stalls', MAX_REDIRECTS, hangingRequestFn); + assert.equal(reqs.length, 1); + reqs[0].emit('timeout'); // simulate the timer configured via the `timeout` option firing + + await assert.rejects( + promise, + new RegExp(`timed out after ${DOWNLOAD_TIMEOUT_MS}ms fetching https://example\\.invalid/stalls`), + ); + }); +});