diff --git a/extensions/entity-files/packages/simple-file-server/package-lock.json b/extensions/entity-files/packages/simple-file-server/package-lock.json index eabb661d..577cd817 100644 --- a/extensions/entity-files/packages/simple-file-server/package-lock.json +++ b/extensions/entity-files/packages/simple-file-server/package-lock.json @@ -18,6 +18,7 @@ "pino": "^9.7.0", "pino-pretty": "^13.0.0", "sharp": "^0.34.1", + "ssh2-sftp-client": "^12.1.1", "zod": "^3.24.0" }, "bin": { @@ -25,14 +26,15 @@ "sfs-test": "dist/cli/index.js" }, "devDependencies": { - "@types/node": "^22.0.0", + "@types/node": "^24.0.0", "@types/sharp": "^0.31.0", + "@types/ssh2-sftp-client": "^9.0.6", "tsx": "^4.19.0", "typescript": "^5.7.0", - "vitest": "^3.0.0" + "vitest": "^4.0.0" }, "engines": { - "node": ">=18.17.0" + "node": ">=24" } }, "node_modules/@borewit/text-codec": { @@ -45,6 +47,18 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", @@ -55,6 +69,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", @@ -1114,6 +1139,35 @@ "node": ">=8" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", + "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@phc/format": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", @@ -1129,24 +1183,10 @@ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", - "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", - "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", + "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", "cpu": [ "arm64" ], @@ -1155,12 +1195,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", - "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", "cpu": [ "arm64" ], @@ -1169,12 +1212,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", - "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", + "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", "cpu": [ "x64" ], @@ -1183,26 +1229,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", - "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", - "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", "cpu": [ "x64" ], @@ -1211,26 +1246,15 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", - "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", - "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", "cpu": [ "arm" ], @@ -1239,26 +1263,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", - "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", - "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", "cpu": [ "arm64" ], @@ -1267,54 +1280,32 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", - "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", - "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", - "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", - "cpu": [ - "ppc64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", - "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", "cpu": [ "ppc64" ], @@ -1323,40 +1314,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", - "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", - "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", - "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", "cpu": [ "s390x" ], @@ -1365,12 +1331,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", - "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", "cpu": [ "x64" ], @@ -1379,12 +1348,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", - "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", "cpu": [ "x64" ], @@ -1393,26 +1365,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", - "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", - "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", + "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", "cpu": [ "arm64" ], @@ -1421,40 +1382,51 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", - "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", + "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", - "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", - "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", "cpu": [ "x64" ], @@ -1463,21 +1435,24 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", - "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" }, "node_modules/@tokenizer/inflate": { "version": "0.2.7", @@ -1503,6 +1478,17 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1529,13 +1515,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", - "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/sharp": { @@ -1548,40 +1534,78 @@ "@types/node": "*" } }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2-sftp-client": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/ssh2-sftp-client/-/ssh2-sftp-client-9.0.6.tgz", + "integrity": "sha512-4+KvXO/V77y9VjI2op2T8+RCGI/GXQAwR0q5Qkj/EJ5YSeyKszqZP6F8i3H3txYoBqjc7sgorqyvBP3+w1EHyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ssh2": "^1.0.0" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", + "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", "dev": true, "license": "MIT", "dependencies": { + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", + "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", + "@vitest/spy": "4.1.6", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -1593,42 +1617,42 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", + "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "@vitest/utils": "4.1.6", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", + "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", + "@vitest/pretty-format": "4.1.6", + "@vitest/utils": "4.1.6", + "magic-string": "^0.30.21", "pathe": "^2.0.3" }, "funding": { @@ -1636,28 +1660,25 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", + "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@vitest/pretty-format": "4.1.6", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1717,6 +1738,15 @@ "node": ">=16.17.0" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1756,41 +1786,38 @@ "fastq": "^1.17.1" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" } }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "optional": true, "engines": { - "node": ">=18" + "node": ">=10.0.0" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 16" + "node": ">=18" } }, "node_modules/colorette": { @@ -1808,6 +1835,21 @@ "node": ">=18" } }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-type": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", @@ -1821,7 +1863,14 @@ "url": "https://opencollective.com/express" } }, - "node_modules/cookie": { + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", @@ -1834,6 +1883,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -1860,16 +1923,6 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1907,9 +1960,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, @@ -2256,13 +2309,6 @@ "node": ">=10" } }, - "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -2325,12 +2371,266 @@ ], "license": "MIT" }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, - "license": "MIT" + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, "node_modules/magic-string": { "version": "0.30.21", @@ -2391,6 +2691,13 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nan": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", + "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==", + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", @@ -2430,6 +2737,17 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -2455,16 +2773,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2616,6 +2924,20 @@ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -2659,56 +2981,58 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "license": "MIT" }, - "node_modules/rollup": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", - "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "node_modules/rolldown": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", + "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.130.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.4", - "@rollup/rollup-android-arm64": "4.60.4", - "@rollup/rollup-darwin-arm64": "4.60.4", - "@rollup/rollup-darwin-x64": "4.60.4", - "@rollup/rollup-freebsd-arm64": "4.60.4", - "@rollup/rollup-freebsd-x64": "4.60.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", - "@rollup/rollup-linux-arm-musleabihf": "4.60.4", - "@rollup/rollup-linux-arm64-gnu": "4.60.4", - "@rollup/rollup-linux-arm64-musl": "4.60.4", - "@rollup/rollup-linux-loong64-gnu": "4.60.4", - "@rollup/rollup-linux-loong64-musl": "4.60.4", - "@rollup/rollup-linux-ppc64-gnu": "4.60.4", - "@rollup/rollup-linux-ppc64-musl": "4.60.4", - "@rollup/rollup-linux-riscv64-gnu": "4.60.4", - "@rollup/rollup-linux-riscv64-musl": "4.60.4", - "@rollup/rollup-linux-s390x-gnu": "4.60.4", - "@rollup/rollup-linux-x64-gnu": "4.60.4", - "@rollup/rollup-linux-x64-musl": "4.60.4", - "@rollup/rollup-openbsd-x64": "4.60.4", - "@rollup/rollup-openharmony-arm64": "4.60.4", - "@rollup/rollup-win32-arm64-msvc": "4.60.4", - "@rollup/rollup-win32-ia32-msvc": "4.60.4", - "@rollup/rollup-win32-x64-gnu": "4.60.4", - "@rollup/rollup-win32-x64-msvc": "4.60.4", - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup/node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, + "@rolldown/binding-android-arm64": "1.0.1", + "@rolldown/binding-darwin-arm64": "1.0.1", + "@rolldown/binding-darwin-x64": "1.0.1", + "@rolldown/binding-freebsd-x64": "1.0.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", + "@rolldown/binding-linux-arm64-gnu": "1.0.1", + "@rolldown/binding-linux-arm64-musl": "1.0.1", + "@rolldown/binding-linux-ppc64-gnu": "1.0.1", + "@rolldown/binding-linux-s390x-gnu": "1.0.1", + "@rolldown/binding-linux-x64-gnu": "1.0.1", + "@rolldown/binding-linux-x64-musl": "1.0.1", + "@rolldown/binding-openharmony-arm64": "1.0.1", + "@rolldown/binding-wasm32-wasi": "1.0.1", + "@rolldown/binding-win32-arm64-msvc": "1.0.1", + "@rolldown/binding-win32-x64-msvc": "1.0.1" + } + }, + "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==", + "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/safe-regex2": { @@ -2742,6 +3066,12 @@ "node": ">=10" } }, + "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/secure-json-parse": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", @@ -2861,6 +3191,40 @@ "node": ">= 10.x" } }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, + "node_modules/ssh2-sftp-client": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/ssh2-sftp-client/-/ssh2-sftp-client-12.1.1.tgz", + "integrity": "sha512-wYVDgwkpcKG2iPGQQ+QR33xkWqLFIaVrYvA+uON4pmxTPaPuB81f1aooUEPN75e/9DCK6rrKYXb6zR6zP3+EtA==", + "license": "Apache-2.0", + "dependencies": { + "concat-stream": "^2.0.0", + "ssh2": "^1.16.0" + }, + "engines": { + "node": ">=18.20.4" + }, + "funding": { + "type": "individual", + "url": "https://square.link/u/4g7sPflL" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -2878,12 +3242,21 @@ } }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/strip-json-comments": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", @@ -2896,19 +3269,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/strtok3": { "version": "10.3.5", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", @@ -2942,11 +3302,14 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { "version": "0.2.16", @@ -2965,30 +3328,10 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -3057,6 +3400,12 @@ "fsevents": "~2.3.3" } }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/type-is": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", @@ -3075,6 +3424,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3102,12 +3457,18 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, + "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==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -3118,18 +3479,17 @@ } }, "node_modules/vite": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", - "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", + "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.1", + "tinyglobby": "^0.2.16" }, "bin": { "vite": "bin/vite.js" @@ -3145,9 +3505,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -3160,13 +3521,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { "optional": true }, - "lightningcss": { + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -3192,573 +3556,80 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" - } - }, "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", + "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", + "@vitest/expect": "4.1.6", + "@vitest/mocker": "4.1.6", + "@vitest/pretty-format": "4.1.6", + "@vitest/runner": "4.1.6", + "@vitest/snapshot": "4.1.6", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.6", + "@vitest/browser-preview": "4.1.6", + "@vitest/browser-webdriverio": "4.1.6", + "@vitest/coverage-istanbul": "4.1.6", + "@vitest/coverage-v8": "4.1.6", + "@vitest/ui": "4.1.6", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -3769,6 +3640,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, diff --git a/extensions/entity-files/packages/simple-file-server/package.json b/extensions/entity-files/packages/simple-file-server/package.json index 0df586af..6b1fc40e 100644 --- a/extensions/entity-files/packages/simple-file-server/package.json +++ b/extensions/entity-files/packages/simple-file-server/package.json @@ -61,11 +61,13 @@ "pino": "^9.7.0", "pino-pretty": "^13.0.0", "sharp": "^0.34.1", + "ssh2-sftp-client": "^12.1.1", "zod": "^3.24.0" }, "devDependencies": { "@types/node": "^24.0.0", "@types/sharp": "^0.31.0", + "@types/ssh2-sftp-client": "^9.0.6", "tsx": "^4.19.0", "typescript": "^5.7.0", "vitest": "^4.0.0" diff --git a/extensions/entity-files/packages/simple-file-server/src/cli/index.ts b/extensions/entity-files/packages/simple-file-server/src/cli/index.ts index 88308cf9..4ab547ed 100644 --- a/extensions/entity-files/packages/simple-file-server/src/cli/index.ts +++ b/extensions/entity-files/packages/simple-file-server/src/cli/index.ts @@ -44,13 +44,51 @@ program const create = program.command('create').description('Create resources') create - .command('bucket ') - .description('Create a new bucket mapped to an absolute filesystem path') + .command('bucket ') + .description('Create a new bucket mapped to an absolute path (local filesystem or SFTP)') .option('--data-dir ', 'Data directory', getDataDir()) - .action(async (name: string, absolutePath: string, opts) => { + .option('--storage ', 'Storage target: local (default) or sftp', 'local') + .option('--sftp-host ', 'SFTP server hostname (required for sftp storage)') + .option('--sftp-port ', 'SFTP server port (default: 22)', '22') + .option('--sftp-username ', 'SFTP username (required for sftp storage)') + .option('--sftp-password ', 'SFTP password') + .option('--sftp-private-key ', 'PEM-encoded private key content for SFTP authentication') + .option('--sftp-passphrase ', 'Passphrase for an encrypted SFTP private key') + .action(async (name: string, bucketPath: string, opts) => { const runtime = await createRuntime({ dataDir: opts.dataDir }) try { - const bucket = await runtime.bucketService.create(name, absolutePath) + const storageTarget = opts.storage as 'local' | 'sftp' + let bucket: Bucket + + if (storageTarget === 'sftp') { + if (!opts.sftpHost) { + console.error(JSON.stringify({ ok: false, error: { code: 'SFS_INVALID_ARGS', message: '--sftp-host is required for sftp storage' } })) + process.exit(1) + } + if (!opts.sftpUsername) { + console.error(JSON.stringify({ ok: false, error: { code: 'SFS_INVALID_ARGS', message: '--sftp-username is required for sftp storage' } })) + process.exit(1) + } + if (!opts.sftpPassword && !opts.sftpPrivateKey) { + console.error(JSON.stringify({ ok: false, error: { code: 'SFS_INVALID_ARGS', message: 'Either --sftp-password or --sftp-private-key is required for sftp storage' } })) + process.exit(1) + } + + bucket = await runtime.bucketService.create(name, bucketPath, 'sftp', { + host: opts.sftpHost as string, + port: parseInt(opts.sftpPort as string, 10), + username: opts.sftpUsername as string, + password: opts.sftpPassword as string | undefined, + privateKey: opts.sftpPrivateKey as string | undefined, + passphrase: opts.sftpPassphrase as string | undefined, + }) + + // Initialise SFTP bucket (creates remote dir + local staging) + await runtime.storageService.initBucket(bucket) + } else { + bucket = await runtime.bucketService.create(name, bucketPath) + } + console.log(JSON.stringify({ ok: true, data: bucket }, null, 2)) } catch (err: unknown) { printError(err) diff --git a/extensions/entity-files/packages/simple-file-server/src/errors/index.ts b/extensions/entity-files/packages/simple-file-server/src/errors/index.ts index a21d7772..5c5de88a 100644 --- a/extensions/entity-files/packages/simple-file-server/src/errors/index.ts +++ b/extensions/entity-files/packages/simple-file-server/src/errors/index.ts @@ -27,6 +27,11 @@ export enum SFSErrorCode { UPLOAD_FAILED = 'SFS_UPLOAD_FAILED', UPLOAD_STAGING_FAILED = 'SFS_UPLOAD_STAGING_FAILED', + // SFTP + SFTP_CONNECTION_FAILED = 'SFS_SFTP_CONNECTION_FAILED', + SFTP_AUTH_FAILED = 'SFS_SFTP_AUTH_FAILED', + SFTP_OPERATION_FAILED = 'SFS_SFTP_OPERATION_FAILED', + // General INTERNAL_ERROR = 'SFS_INTERNAL_ERROR', NOT_IMPLEMENTED = 'SFS_NOT_IMPLEMENTED', diff --git a/extensions/entity-files/packages/simple-file-server/src/index.ts b/extensions/entity-files/packages/simple-file-server/src/index.ts index c28cfcac..d35bbe4f 100644 --- a/extensions/entity-files/packages/simple-file-server/src/index.ts +++ b/extensions/entity-files/packages/simple-file-server/src/index.ts @@ -2,6 +2,10 @@ export { createRuntime, startServer } from './runtime/index.js' export { BucketService } from './storage/bucket.service.js' export { StorageService } from './storage/storage.service.js' +export { LocalStorageProvider } from './storage/local.provider.js' +export { SftpStorageProvider } from './storage/sftp.provider.js' +export { StorageProviderRegistry } from './storage/storage.provider.js' +export type { StorageProvider, DownloadResult, UploadOptions } from './storage/storage.provider.js' export { ThumbnailService } from './thumbnail/thumbnail.service.js' export { IdentityService } from './auth/identity.service.js' export { OperationalLogger } from './logging/logger.js' diff --git a/extensions/entity-files/packages/simple-file-server/src/runtime/index.ts b/extensions/entity-files/packages/simple-file-server/src/runtime/index.ts index c34146d4..1d97d749 100644 --- a/extensions/entity-files/packages/simple-file-server/src/runtime/index.ts +++ b/extensions/entity-files/packages/simple-file-server/src/runtime/index.ts @@ -1,7 +1,9 @@ -import { ensureDataDirs, getDefaultConfig } from '../config/config.service.js' +import path from 'node:path' +import { ensureDataDirs, getDefaultConfig, resolveDataPaths } from '../config/config.service.js' import { IdentityService } from '../auth/identity.service.js' import { BucketService } from '../storage/bucket.service.js' import { StorageService } from '../storage/storage.service.js' +import { SftpStorageProvider } from '../storage/sftp.provider.js' import { ThumbnailService } from '../thumbnail/thumbnail.service.js' import { OperationalLogger } from '../logging/logger.js' import { createServer } from '../http/server.js' @@ -29,6 +31,11 @@ export async function createRuntime(overrides?: Partial): Promise): Promise): Promise { - if (!path.isAbsolute(absolutePath)) { - throw new SFSError(SFSErrorCode.BUCKET_PATH_INVALID, `Bucket path must be absolute: ${absolutePath}`, 400) + /** + * Create a new bucket. + * + * @param name Unique bucket name. + * @param bucketPath Absolute path to the storage root. + * For `'local'` this is a local filesystem path. + * For `'sftp'` this is the remote base path on the SFTP server. + * @param storageTarget Storage backend (default: `'local'`). + * @param sftpConfig Required when `storageTarget` is `'sftp'`. + */ + async create( + name: string, + bucketPath: string, + storageTarget: StorageTarget = 'local', + sftpConfig?: SftpConfig, + ): Promise { + if (!path.isAbsolute(bucketPath)) { + throw new SFSError(SFSErrorCode.BUCKET_PATH_INVALID, `Bucket path must be absolute: ${bucketPath}`, 400) } const existing = await this.find(name) @@ -33,22 +48,45 @@ export class BucketService { throw conflict(`Bucket '${name}' already exists`, SFSErrorCode.BUCKET_ALREADY_EXISTS) } + if (storageTarget === 'sftp') { + if (!sftpConfig) { + throw new SFSError( + SFSErrorCode.BUCKET_PATH_INVALID, + `sftpConfig is required when storageTarget is 'sftp'`, + 400, + ) + } + + const bucket: Bucket = { + name, + path: bucketPath, + createdAt: new Date().toISOString(), + storageTarget: 'sftp', + sftpConfig, + } + + await writeJsonFile(this.bucketFilePath(name), bucket) + return bucket + } + + // ── local storage ────────────────────────────────────────────────────── + // Ensure bucket dir exists - await fsp.mkdir(absolutePath, { recursive: true }) + await fsp.mkdir(bucketPath, { recursive: true }) // Validate writable try { - await fsp.access(absolutePath, fs.constants.W_OK) + await fsp.access(bucketPath, fs.constants.W_OK) } catch { - throw new SFSError(SFSErrorCode.BUCKET_PATH_NOT_WRITABLE, `Bucket path is not writable: ${absolutePath}`, 400) + throw new SFSError(SFSErrorCode.BUCKET_PATH_NOT_WRITABLE, `Bucket path is not writable: ${bucketPath}`, 400) } // Create internal .sfs directory - await this.ensureBucketInternals(absolutePath) + await this.ensureBucketInternals(bucketPath) const bucket: Bucket = { name, - path: absolutePath, + path: bucketPath, createdAt: new Date().toISOString(), } @@ -94,6 +132,7 @@ export class BucketService { /** * Resolve a key to an absolute filesystem path within the bucket. + * Only valid for `'local'` storage target buckets. * Validates against path traversal attacks. */ resolveKey(bucket: Bucket, key: string): string { @@ -139,38 +178,5 @@ export class BucketService { getThumbsDir(bucket: Bucket): string { return path.join(bucket.path, SFS_INTERNAL_DIR, 'thumbs') } - - /** - * Validate startup: check all buckets are accessible and writable, clean orphan staging files. - */ - async validateStartup(): Promise { - const buckets = await this.list() - for (const bucket of buckets) { - try { - await fsp.access(bucket.path, fs.constants.W_OK) - await this.ensureBucketInternals(bucket.path) - await this.cleanOrphanStagingFiles(bucket) - } catch (err) { - console.warn(`[SFS] Warning: bucket '${bucket.name}' at '${bucket.path}' is not accessible:`, err) - } - } - } - - private async cleanOrphanStagingFiles(bucket: Bucket): Promise { - const stagingDir = this.getStagingDir(bucket) - try { - const files = await fsp.readdir(stagingDir) - const cutoff = Date.now() - 60 * 60 * 1000 // older than 1 hour - for (const file of files) { - const filePath = path.join(stagingDir, file) - const stat = await fsp.stat(filePath) - if (stat.mtimeMs < cutoff) { - await fsp.unlink(filePath).catch(() => {}) - } - } - } catch { - // Ignore cleanup errors - } - } } diff --git a/extensions/entity-files/packages/simple-file-server/src/storage/local.provider.ts b/extensions/entity-files/packages/simple-file-server/src/storage/local.provider.ts new file mode 100644 index 00000000..fa2a1e97 --- /dev/null +++ b/extensions/entity-files/packages/simple-file-server/src/storage/local.provider.ts @@ -0,0 +1,293 @@ +import path from 'node:path' +import fsp from 'node:fs/promises' +import fs from 'node:fs' +import crypto from 'node:crypto' +import { pipeline } from 'node:stream/promises' +import type { Readable } from 'node:stream' +import { fileTypeFromFile } from 'file-type' +import type { Bucket, ListResult, ListEntry } from '../types/index.js' +import { SFSError, SFSErrorCode, notFound } from '../errors/index.js' +import type { StorageProvider, DownloadResult, UploadOptions } from './storage.provider.js' +import type { BucketService } from './bucket.service.js' + +/** + * Storage provider that persists files on the local filesystem. + * + * This is the default backend. Files are stored under the bucket's `path` + * directory, uploads are staged atomically, and internal state lives inside a + * reserved `.sfs` sub-directory. + */ +export class LocalStorageProvider implements StorageProvider { + constructor(private readonly bucketService: BucketService) {} + + async initBucket(bucket: Bucket): Promise { + await this.bucketService.ensureBucketInternals(bucket.path) + } + + async validateBucket(bucket: Bucket): Promise { + try { + await fsp.access(bucket.path, fs.constants.W_OK) + } catch { + console.warn(`[SFS] Warning: bucket '${bucket.name}' at '${bucket.path}' is not accessible`) + return + } + await this.bucketService.ensureBucketInternals(bucket.path) + await this.cleanOrphanStagingFiles(bucket) + } + + async download(bucket: Bucket, key: string): Promise { + const filePath = this.bucketService.resolveKey(bucket, key) + + let stat: fs.Stats + try { + stat = await fsp.stat(filePath) + } catch { + throw notFound(`Object '${key}' in bucket '${bucket.name}'`, SFSErrorCode.OBJECT_NOT_FOUND) + } + + if (stat.isDirectory()) { + throw new SFSError(SFSErrorCode.OBJECT_IS_DIRECTORY, `'${key}' is a directory, not a file`, 400) + } + + const mimeType = await this.detectMimeType(filePath) + const etag = `"${stat.size}-${stat.mtimeMs}"` + + return { + stream: fs.createReadStream(filePath), + size: stat.size, + mimeType, + lastModified: stat.mtime, + etag, + } + } + + async upload( + bucket: Bucket, + key: string, + source: Readable, + options: UploadOptions = {}, + ): Promise<{ size: number; etag: string }> { + const finalPath = this.bucketService.resolveKey(bucket, key) + const stagingDir = this.bucketService.getStagingDir(bucket) + const stagingFile = path.join(stagingDir, `${crypto.randomUUID()}.tmp`) + + // Ensure parent directory exists + const parentDir = path.dirname(finalPath) + await fsp.mkdir(parentDir, { recursive: true }) + + let bytesWritten = 0 + + try { + // Stream upload to staging + const writeStream = fs.createWriteStream(stagingFile) + writeStream.on('pipe', () => {}) + + await pipeline(source, writeStream) + + // fsync to ensure data is on disk + const fd = await fsp.open(stagingFile, 'r') + try { + await fd.sync() + } finally { + await fd.close() + } + + const stat = await fsp.stat(stagingFile) + bytesWritten = stat.size + + // Atomic move to final destination + try { + await fsp.rename(stagingFile, finalPath) + } catch (err: unknown) { + // Cross-device rename: fallback to copy + unlink + const error = err as NodeJS.ErrnoException + if (error.code === 'EXDEV') { + await pipeline(fs.createReadStream(stagingFile), fs.createWriteStream(finalPath)) + await fsp.unlink(stagingFile) + } else { + throw err + } + } + + const etag = `"${bytesWritten}-${Date.now()}"` + return { size: bytesWritten, etag } + } catch (err) { + // Cleanup staging file on error + await fsp.unlink(stagingFile).catch(() => {}) + throw err + } + } + + async delete(bucket: Bucket, key: string): Promise { + const filePath = this.bucketService.resolveKey(bucket, key) + + let stat: fs.Stats + try { + stat = await fsp.stat(filePath) + } catch { + throw notFound(`Object '${key}' in bucket '${bucket.name}'`, SFSErrorCode.OBJECT_NOT_FOUND) + } + + if (stat.isDirectory()) { + throw new SFSError(SFSErrorCode.OBJECT_IS_DIRECTORY, `'${key}' is a directory and cannot be deleted`, 400) + } + + await fsp.unlink(filePath) + } + + async listDirectory(bucket: Bucket, keyPrefix: string): Promise { + const dirPath = this.bucketService.resolveKey(bucket, keyPrefix || '/') + + let stat: fs.Stats + try { + stat = await fsp.stat(dirPath) + } catch { + throw notFound(`Directory '${keyPrefix}' in bucket '${bucket.name}'`, SFSErrorCode.OBJECT_NOT_FOUND) + } + + if (!stat.isDirectory()) { + throw new SFSError(SFSErrorCode.OBJECT_IS_DIRECTORY, `'${keyPrefix}' is not a directory`, 404) + } + + const entries = await fsp.readdir(dirPath, { withFileTypes: true }) + const result: ListEntry[] = [] + + for (const entry of entries) { + // Skip internal .sfs directory + if (entry.name === '.sfs') continue + + const entryPath = path.join(dirPath, entry.name) + try { + const entryStat = await fsp.stat(entryPath) + const relativeKey = path.join(keyPrefix || '', entry.name).replace(/\\/g, '/') + + result.push({ + key: entry.isDirectory() ? `${relativeKey}/` : relativeKey, + size: entryStat.size, + lastModified: entryStat.mtime.toISOString(), + isDirectory: entry.isDirectory(), + }) + } catch { + // Skip entries we can't stat + } + } + + return { + bucket: bucket.name, + prefix: keyPrefix || '/', + entries: result, + hasMore: false, + total: result.length, + } + } + + async listBucket(bucket: Bucket, limit: number = 100, cursor?: string): Promise { + const allEntries = await this.collectAllEntries(bucket, bucket.path, '') + + let startIdx = 0 + if (cursor) { + const cursorIdx = allEntries.findIndex(e => e.key === cursor) + if (cursorIdx >= 0) startIdx = cursorIdx + 1 + } + + const page = allEntries.slice(startIdx, startIdx + limit) + const hasMore = startIdx + limit < allEntries.length + const nextCursor = hasMore ? page[page.length - 1]?.key : undefined + + return { + bucket: bucket.name, + prefix: '/', + entries: page, + cursor: nextCursor, + hasMore, + total: allEntries.length, + } + } + + private async collectAllEntries(bucket: Bucket, dirPath: string, prefix: string): Promise { + const result: ListEntry[] = [] + try { + const entries = await fsp.readdir(dirPath, { withFileTypes: true }) + for (const entry of entries) { + if (entry.name === '.sfs') continue + + const entryPath = path.join(dirPath, entry.name) + const relKey = prefix ? `${prefix}/${entry.name}` : entry.name + + try { + const stat = await fsp.stat(entryPath) + if (entry.isDirectory()) { + const subEntries = await this.collectAllEntries(bucket, entryPath, relKey) + result.push(...subEntries) + } else { + result.push({ + key: relKey, + size: stat.size, + lastModified: stat.mtime.toISOString(), + isDirectory: false, + }) + } + } catch { + // skip + } + } + } catch { + // empty + } + return result + } + + private async detectMimeType(filePath: string): Promise { + try { + const result = await fileTypeFromFile(filePath) + if (result) return result.mime + } catch { + // fallback + } + return this.mimeFromExtension(filePath) + } + + private mimeFromExtension(filePath: string): string { + const ext = path.extname(filePath).toLowerCase() + const mimeMap: Record = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml', + '.pdf': 'application/pdf', + '.json': 'application/json', + '.txt': 'text/plain', + '.html': 'text/html', + '.css': 'text/css', + '.js': 'application/javascript', + '.ts': 'application/typescript', + '.xml': 'application/xml', + '.zip': 'application/zip', + '.tar': 'application/x-tar', + '.gz': 'application/gzip', + '.mp4': 'video/mp4', + '.mp3': 'audio/mpeg', + '.wav': 'audio/wav', + } + return mimeMap[ext] ?? 'application/octet-stream' + } + + private async cleanOrphanStagingFiles(bucket: Bucket): Promise { + const stagingDir = this.bucketService.getStagingDir(bucket) + try { + const files = await fsp.readdir(stagingDir) + const cutoff = Date.now() - 60 * 60 * 1000 // older than 1 hour + for (const file of files) { + const filePath = path.join(stagingDir, file) + const stat = await fsp.stat(filePath) + if (stat.mtimeMs < cutoff) { + await fsp.unlink(filePath).catch(() => {}) + } + } + } catch { + // Ignore cleanup errors + } + } +} diff --git a/extensions/entity-files/packages/simple-file-server/src/storage/sftp.provider.ts b/extensions/entity-files/packages/simple-file-server/src/storage/sftp.provider.ts new file mode 100644 index 00000000..b9f51e5f --- /dev/null +++ b/extensions/entity-files/packages/simple-file-server/src/storage/sftp.provider.ts @@ -0,0 +1,362 @@ +import path from 'node:path' +import fsp from 'node:fs/promises' +import fs from 'node:fs' +import crypto from 'node:crypto' +import { pipeline } from 'node:stream/promises' +import { PassThrough } from 'node:stream' +import type { Readable } from 'node:stream' +import SftpClient from 'ssh2-sftp-client' +import type { Bucket, ListEntry, ListResult, SftpConfig } from '../types/index.js' +import { SFSError, SFSErrorCode, notFound } from '../errors/index.js' +import type { StorageProvider, DownloadResult, UploadOptions } from './storage.provider.js' + +/** Remove trailing forward-slashes without using a backtracking regex. */ +function trimTrailingSlashes(s: string): string { + let end = s.length + while (end > 0 && s[end - 1] === '/') end-- + return s.slice(0, end) +} + +/** + * Storage provider that stores files on a remote SFTP server. + * + * One connection is maintained per bucket (keyed by bucket name) and + * automatically re-established when a connection is lost. + * + * Files are staged locally before being pushed to the SFTP server so that + * partial uploads are never visible at the final destination. + * + * Thumbnails are **not** supported for SFTP buckets — the {@link ThumbnailService} + * will skip such buckets automatically. + * + * Example registration: + *
{@code
+ * registry.register('sftp', new SftpStorageProvider('/var/sfs/sftp-staging'))
+ * }
+ */ +export class SftpStorageProvider implements StorageProvider { + private readonly connections = new Map() + + /** + * @param localCacheDir Base directory for local staging files. + * Typically `{dataDir}/sftp-staging`. + */ + constructor(private readonly localCacheDir: string) {} + + // ── Connection management ───────────────────────────────────────────────── + + private async getClient(bucket: Bucket): Promise { + const config = this.requireSftpConfig(bucket) + const key = bucket.name + + const existing = this.connections.get(key) + if (existing) { + // Perform a lightweight liveness check before reusing + try { + await existing.cwd() + return existing + } catch { + // Dead connection — fall through to reconnect + this.connections.delete(key) + } + } + + const client = new SftpClient() + const connectOptions: SftpClient.ConnectOptions = { + host: config.host, + port: config.port ?? 22, + username: config.username, + } + if (config.password) connectOptions.password = config.password + if (config.privateKey) connectOptions.privateKey = config.privateKey + if (config.passphrase) connectOptions.passphrase = config.passphrase + + try { + await client.connect(connectOptions) + } catch (err) { + throw new SFSError( + SFSErrorCode.SFTP_CONNECTION_FAILED, + `Failed to connect to SFTP server for bucket '${bucket.name}': ${err instanceof Error ? err.message : String(err)}`, + 503, + ) + } + + this.connections.set(key, client) + return client + } + + private requireSftpConfig(bucket: Bucket): SftpConfig { + if (!bucket.sftpConfig) { + throw new SFSError( + SFSErrorCode.BUCKET_PATH_INVALID, + `Bucket '${bucket.name}' has storageTarget 'sftp' but no sftpConfig`, + 500, + ) + } + return bucket.sftpConfig + } + + // ── Path helpers ────────────────────────────────────────────────────────── + + private remotePath(bucket: Bucket, key: string): string { + const normalized = key.replace(/\\/g, '/').replace(/^\/+/, '') + + // Prevent path traversal + const parts = normalized.split('/') + if (parts.some(p => p === '..' || p === '.')) { + throw new SFSError(SFSErrorCode.PATH_TRAVERSAL, 'Path traversal detected in SFTP key', 400) + } + + const base = trimTrailingSlashes(bucket.path) + return normalized ? `${base}/${normalized}` : base + } + + private localStagingDir(bucket: Bucket): string { + return path.join(this.localCacheDir, bucket.name, 'staging') + } + + // ── StorageProvider implementation ─────────────────────────────────────── + + async initBucket(bucket: Bucket): Promise { + // Ensure local staging directory exists + await fsp.mkdir(this.localStagingDir(bucket), { recursive: true }) + + // Ensure remote base directory exists + const client = await this.getClient(bucket) + await client.mkdir(bucket.path, true).catch(() => { + // mkdir may fail if directory already exists on some servers — safe to ignore + }) + } + + async validateBucket(bucket: Bucket): Promise { + await fsp.mkdir(this.localStagingDir(bucket), { recursive: true }) + const client = await this.getClient(bucket) + try { + await client.stat(bucket.path) + } catch { + console.warn(`[SFS] Warning: SFTP bucket '${bucket.name}' remote path '${bucket.path}' is not accessible`) + } + await this.cleanOrphanStagingFiles(bucket) + } + + async download(bucket: Bucket, key: string): Promise { + const remote = this.remotePath(bucket, key) + const client = await this.getClient(bucket) + + let remoteStat: SftpClient.FileStats + try { + remoteStat = await client.stat(remote) + } catch { + throw notFound(`Object '${key}' in bucket '${bucket.name}'`, SFSErrorCode.OBJECT_NOT_FOUND) + } + + if (remoteStat.isDirectory) { + throw new SFSError(SFSErrorCode.OBJECT_IS_DIRECTORY, `'${key}' is a directory, not a file`, 400) + } + + const size = remoteStat.size ?? 0 + const modifyTime = remoteStat.modifyTime ?? Date.now() + const lastModified = new Date(modifyTime) + const etag = `"${size}-${modifyTime}"` + const mimeType = this.mimeFromKey(key) + + // Pipe the SFTP download into a PassThrough so the caller gets a Readable + const passThrough = new PassThrough() + client.get(remote, passThrough).catch((err: unknown) => { + passThrough.destroy(err instanceof Error ? err : new Error(String(err))) + }) + + return { stream: passThrough, size, mimeType, lastModified, etag } + } + + async upload( + bucket: Bucket, + key: string, + source: Readable, + _options: UploadOptions = {}, + ): Promise<{ size: number; etag: string }> { + const remote = this.remotePath(bucket, key) + const stagingDir = this.localStagingDir(bucket) + const stagingFile = path.join(stagingDir, `${crypto.randomUUID()}.tmp`) + + await fsp.mkdir(stagingDir, { recursive: true }) + + try { + // Stage the upload to a local temporary file + const writeStream = fs.createWriteStream(stagingFile) + await pipeline(source, writeStream) + + // fsync to ensure data is persisted before the SFTP push + const fd = await fsp.open(stagingFile, 'r') + try { + await fd.sync() + } finally { + await fd.close() + } + + const stat = await fsp.stat(stagingFile) + const bytesWritten = stat.size + + // Ensure the remote parent directory exists + const remoteDir = remote.split('/').slice(0, -1).join('/') + const client = await this.getClient(bucket) + if (remoteDir && remoteDir !== trimTrailingSlashes(bucket.path)) { + await client.mkdir(remoteDir, true).catch(() => {}) + } + + // Push staged file to SFTP + await client.put(stagingFile, remote) + + const etag = `"${bytesWritten}-${Date.now()}"` + return { size: bytesWritten, etag } + } finally { + await fsp.unlink(stagingFile).catch(() => {}) + } + } + + async delete(bucket: Bucket, key: string): Promise { + const remote = this.remotePath(bucket, key) + const client = await this.getClient(bucket) + try { + await client.delete(remote) + } catch { + throw notFound(`Object '${key}' in bucket '${bucket.name}'`, SFSErrorCode.OBJECT_NOT_FOUND) + } + } + + async listDirectory(bucket: Bucket, keyPrefix: string): Promise { + const remoteDir = this.remotePath(bucket, keyPrefix || '/') + const client = await this.getClient(bucket) + + let entries: SftpClient.FileInfo[] + try { + entries = await client.list(remoteDir) + } catch { + throw notFound(`Directory '${keyPrefix}' in bucket '${bucket.name}'`, SFSErrorCode.OBJECT_NOT_FOUND) + } + + const result: ListEntry[] = entries.map(entry => { + const prefix = keyPrefix ? `${keyPrefix.replace(/\/$/, '')}/` : '' + const isDir = entry.type === 'd' + const key = isDir ? `${prefix}${entry.name}/` : `${prefix}${entry.name}` + return { + key, + size: entry.size ?? 0, + lastModified: new Date(entry.modifyTime ?? Date.now()).toISOString(), + isDirectory: isDir, + } + }) + + return { + bucket: bucket.name, + prefix: keyPrefix || '/', + entries: result, + hasMore: false, + total: result.length, + } + } + + async listBucket(bucket: Bucket, limit: number = 100, cursor?: string): Promise { + const allEntries = await this.collectAllEntries(bucket, bucket.path, '') + + let startIdx = 0 + if (cursor) { + const cursorIdx = allEntries.findIndex(e => e.key === cursor) + if (cursorIdx >= 0) startIdx = cursorIdx + 1 + } + + const page = allEntries.slice(startIdx, startIdx + limit) + const hasMore = startIdx + limit < allEntries.length + const nextCursor = hasMore ? page[page.length - 1]?.key : undefined + + return { + bucket: bucket.name, + prefix: '/', + entries: page, + cursor: nextCursor, + hasMore, + total: allEntries.length, + } + } + + async close(): Promise { + for (const client of this.connections.values()) { + await client.end().catch(() => {}) + } + this.connections.clear() + } + + // ── Private helpers ──────────────────────────────────────────────────────── + + private async collectAllEntries(bucket: Bucket, dirPath: string, prefix: string): Promise { + const client = await this.getClient(bucket) + const result: ListEntry[] = [] + + try { + const entries = await client.list(dirPath) + for (const entry of entries) { + const relKey = prefix ? `${prefix}/${entry.name}` : entry.name + if (entry.type === 'd') { + const subDir = `${trimTrailingSlashes(dirPath)}/${entry.name}` + const subEntries = await this.collectAllEntries(bucket, subDir, relKey) + result.push(...subEntries) + } else { + result.push({ + key: relKey, + size: entry.size ?? 0, + lastModified: new Date(entry.modifyTime ?? Date.now()).toISOString(), + isDirectory: false, + }) + } + } + } catch { + // empty or inaccessible directory + } + + return result + } + + private mimeFromKey(key: string): string { + const ext = path.extname(key).toLowerCase() + const mimeMap: Record = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml', + '.pdf': 'application/pdf', + '.json': 'application/json', + '.txt': 'text/plain', + '.html': 'text/html', + '.css': 'text/css', + '.js': 'application/javascript', + '.ts': 'application/typescript', + '.xml': 'application/xml', + '.zip': 'application/zip', + '.tar': 'application/x-tar', + '.gz': 'application/gzip', + '.mp4': 'video/mp4', + '.mp3': 'audio/mpeg', + '.wav': 'audio/wav', + } + return mimeMap[ext] ?? 'application/octet-stream' + } + + private async cleanOrphanStagingFiles(bucket: Bucket): Promise { + const stagingDir = this.localStagingDir(bucket) + try { + const files = await fsp.readdir(stagingDir) + const cutoff = Date.now() - 60 * 60 * 1000 + for (const file of files) { + const filePath = path.join(stagingDir, file) + const stat = await fsp.stat(filePath) + if (stat.mtimeMs < cutoff) { + await fsp.unlink(filePath).catch(() => {}) + } + } + } catch { + // ignore cleanup errors + } + } +} diff --git a/extensions/entity-files/packages/simple-file-server/src/storage/storage.provider.ts b/extensions/entity-files/packages/simple-file-server/src/storage/storage.provider.ts new file mode 100644 index 00000000..69a4c556 --- /dev/null +++ b/extensions/entity-files/packages/simple-file-server/src/storage/storage.provider.ts @@ -0,0 +1,102 @@ +import type { Readable } from 'node:stream' +import type { Bucket, ListResult } from '../types/index.js' +import { SFSError, SFSErrorCode } from '../errors/index.js' + +export interface DownloadResult { + stream: Readable + size: number + mimeType: string + lastModified: Date + etag: string +} + +export interface UploadOptions { + overwrite?: boolean +} + +/** + * Abstraction over a storage backend. + * Implementations must be registered with a {@link StorageProviderRegistry} so that + * {@link StorageService} can dispatch operations to the correct backend. + */ +export interface StorageProvider { + /** + * Called once after a bucket is created to initialise any remote or local + * resources (e.g., create the remote directory and local staging area). + */ + initBucket(bucket: Bucket): Promise + + /** + * Called on server startup to verify that the bucket is accessible and + * healthy. Implementations may also perform cleanup (e.g., orphan staging + * files) inside this method. + */ + validateBucket(bucket: Bucket): Promise + + /** Download a file from the bucket. Returns a readable stream. */ + download(bucket: Bucket, key: string): Promise + + /** Upload a file to the bucket using staging + atomic commit. */ + upload(bucket: Bucket, key: string, source: Readable, options?: UploadOptions): Promise<{ size: number; etag: string }> + + /** Delete a single file from the bucket. */ + delete(bucket: Bucket, key: string): Promise + + /** List the immediate contents of a directory inside the bucket. */ + listDirectory(bucket: Bucket, keyPrefix: string): Promise + + /** List all objects inside the bucket with pagination. */ + listBucket(bucket: Bucket, limit: number, cursor?: string): Promise + + /** + * Release any persistent resources held by this provider (e.g., open + * network connections). Called on server shutdown. + */ + close?(): Promise +} + +/** + * Registry that maps storage target names to {@link StorageProvider} instances. + * New storage backends can be added at runtime by calling {@link register}. + * + * Example: + *
{@code
+ * const registry = new StorageProviderRegistry()
+ * registry.register('local', new LocalStorageProvider(bucketService))
+ * registry.register('sftp', new SftpStorageProvider(cacheDir))
+ * }
+ */ +export class StorageProviderRegistry { + private readonly providers = new Map() + + /** + * Register a storage provider under a given target name. + * @param target Storage target identifier (e.g., `'local'`, `'sftp'`). + * @param provider Provider implementation. + */ + register(target: string, provider: StorageProvider): void { + this.providers.set(target, provider) + } + + /** + * Retrieve the provider for a given target. + * @param target Storage target identifier. + * @throws {SFSError} when no provider has been registered for `target`. + */ + get(target: string): StorageProvider { + const provider = this.providers.get(target) + if (!provider) { + throw new SFSError( + SFSErrorCode.NOT_IMPLEMENTED, + `No storage provider registered for target '${target}'`, + 500, + ) + } + return provider + } + + /** Returns all registered providers keyed by target name. */ + getAll(): Map { + return new Map(this.providers) + } +} diff --git a/extensions/entity-files/packages/simple-file-server/src/storage/storage.service.ts b/extensions/entity-files/packages/simple-file-server/src/storage/storage.service.ts index aeb367fe..df95525d 100644 --- a/extensions/entity-files/packages/simple-file-server/src/storage/storage.service.ts +++ b/extensions/entity-files/packages/simple-file-server/src/storage/storage.service.ts @@ -1,247 +1,133 @@ import path from 'node:path' -import fsp from 'node:fs/promises' -import fs from 'node:fs' -import type { Stats } from 'node:fs' -import crypto from 'node:crypto' -import { pipeline } from 'node:stream/promises' +import { fileTypeFromBuffer } from 'file-type' import type { Readable } from 'node:stream' -import { fileTypeFromBuffer, fileTypeFromFile } from 'file-type' -import type { Bucket, ListResult, ListEntry } from '../types/index.js' -import { SFSError, SFSErrorCode, notFound } from '../errors/index.js' -import { BucketService } from './bucket.service.js' +import type { Bucket, ListResult } from '../types/index.js' +import { StorageProviderRegistry } from './storage.provider.js' +import type { StorageProvider, DownloadResult, UploadOptions } from './storage.provider.js' +import { LocalStorageProvider } from './local.provider.js' +import type { BucketService } from './bucket.service.js' + +export type { DownloadResult, UploadOptions } + +/** + * Facade over the {@link StorageProviderRegistry} that routes file operations + * to the correct backend based on each bucket's `storageTarget`. + * + * By default, a `LocalStorageProvider` is registered for the `'local'` target. + * Additional providers (e.g., `SftpStorageProvider`) can be registered via + * {@link registerProvider}. + * + * Example: + *
{@code
+ * const storageService = new StorageService(bucketService)
+ * storageService.registerProvider('sftp', new SftpStorageProvider(cacheDir))
+ * }
+ */ +export class StorageService { + private readonly registry: StorageProviderRegistry -export interface DownloadResult { - stream: fs.ReadStream - size: number - mimeType: string - lastModified: Date - etag: string -} + constructor(private readonly bucketService: BucketService, registry?: StorageProviderRegistry) { + this.registry = registry ?? new StorageProviderRegistry() + // Register the built-in local provider + this.registry.register('local', new LocalStorageProvider(bucketService)) + } -export interface UploadOptions { - overwrite?: boolean -} + /** + * Register an additional storage provider. + * @param target Storage target identifier (e.g., `'sftp'`). + * @param provider The provider implementation. + */ + registerProvider(target: string, provider: StorageProvider): void { + this.registry.register(target, provider) + } -export class StorageService { - constructor(private readonly bucketService: BucketService) {} + private getProvider(bucket: Bucket): StorageProvider { + return this.registry.get(bucket.storageTarget ?? 'local') + } /** - * Download a file from a bucket. Returns a readable stream. + * Initialise a newly-created bucket (creates directories, remote paths, etc.). */ - async download(bucket: Bucket, key: string): Promise { - const filePath = this.bucketService.resolveKey(bucket, key) + async initBucket(bucket: Bucket): Promise { + await this.getProvider(bucket).initBucket(bucket) + } - let stat: Stats - try { - stat = await fsp.stat(filePath) - } catch { - throw notFound(`Object '${key}' in bucket '${bucket.name}'`, SFSErrorCode.OBJECT_NOT_FOUND) + /** + * Validate all registered buckets on startup. + * For local buckets this checks write access and cleans orphan staging files. + * For remote buckets (e.g., SFTP) this verifies connectivity. + */ + async validateStartup(): Promise { + const buckets = await this.bucketService.list() + for (const bucket of buckets) { + try { + await this.getProvider(bucket).validateBucket(bucket) + } catch (err) { + console.warn(`[SFS] Warning: could not validate bucket '${bucket.name}':`, err) + } } + } - if (stat.isDirectory()) { - throw new SFSError(SFSErrorCode.OBJECT_IS_DIRECTORY, `'${key}' is a directory, not a file`, 400) + /** + * Close all provider connections (called on graceful shutdown). + */ + async closeProviders(): Promise { + for (const provider of this.registry.getAll().values()) { + await provider.close?.() } + } - const mimeType = await this.detectMimeType(filePath) - const etag = `"${stat.size}-${stat.mtimeMs}"` + // ── Delegated operations ────────────────────────────────────────────────── - return { - stream: fs.createReadStream(filePath), - size: stat.size, - mimeType, - lastModified: stat.mtime, - etag, - } + /** + * Download a file from a bucket. Returns a readable stream. + */ + async download(bucket: Bucket, key: string): Promise { + return this.getProvider(bucket).download(bucket, key) } /** - * Upload a file to a bucket using staging + atomic move. + * Upload a file to a bucket using staging + atomic commit. */ - async upload(bucket: Bucket, key: string, source: Readable, options: UploadOptions = {}): Promise<{ size: number; etag: string }> { - const finalPath = this.bucketService.resolveKey(bucket, key) - const stagingDir = this.bucketService.getStagingDir(bucket) - const stagingFile = path.join(stagingDir, `${crypto.randomUUID()}.tmp`) - - // Ensure parent directory exists - const parentDir = path.dirname(finalPath) - await fsp.mkdir(parentDir, { recursive: true }) - - let bytesWritten = 0 - - try { - // Stream upload to staging - const writeStream = fs.createWriteStream(stagingFile) - writeStream.on('pipe', () => {}) - - await pipeline(source, writeStream) - - // fsync to ensure data is on disk - const fd = await fsp.open(stagingFile, 'r') - try { - await fd.sync() - } finally { - await fd.close() - } - - const stat = await fsp.stat(stagingFile) - bytesWritten = stat.size - - // Atomic move to final destination - try { - await fsp.rename(stagingFile, finalPath) - } catch (err: unknown) { - // Cross-device rename: fallback to copy + unlink - const error = err as NodeJS.ErrnoException - if (error.code === 'EXDEV') { - await pipeline(fs.createReadStream(stagingFile), fs.createWriteStream(finalPath)) - await fsp.unlink(stagingFile) - } else { - throw err - } - } - - const etag = `"${bytesWritten}-${Date.now()}"` - return { size: bytesWritten, etag } - } catch (err) { - // Cleanup staging file on error - await fsp.unlink(stagingFile).catch(() => {}) - throw err - } + async upload( + bucket: Bucket, + key: string, + source: Readable, + options: UploadOptions = {}, + ): Promise<{ size: number; etag: string }> { + return this.getProvider(bucket).upload(bucket, key, source, options) } /** * Delete a file from a bucket. */ async delete(bucket: Bucket, key: string): Promise { - const filePath = this.bucketService.resolveKey(bucket, key) - - let stat: Stats - try { - stat = await fsp.stat(filePath) - } catch { - throw notFound(`Object '${key}' in bucket '${bucket.name}'`, SFSErrorCode.OBJECT_NOT_FOUND) - } - - if (stat.isDirectory()) { - throw new SFSError(SFSErrorCode.OBJECT_IS_DIRECTORY, `'${key}' is a directory and cannot be deleted`, 400) - } - - await fsp.unlink(filePath) + return this.getProvider(bucket).delete(bucket, key) } /** * List directory contents. */ async listDirectory(bucket: Bucket, keyPrefix: string): Promise { - const dirPath = this.bucketService.resolveKey(bucket, keyPrefix || '/') - - let stat: Stats - try { - stat = await fsp.stat(dirPath) - } catch { - throw notFound(`Directory '${keyPrefix}' in bucket '${bucket.name}'`, SFSErrorCode.OBJECT_NOT_FOUND) - } - - if (!stat.isDirectory()) { - throw new SFSError(SFSErrorCode.OBJECT_IS_DIRECTORY, `'${keyPrefix}' is not a directory`, 404) - } - - const entries = await fsp.readdir(dirPath, { withFileTypes: true }) - const result: ListEntry[] = [] - - for (const entry of entries) { - // Skip internal .sfs directory - if (entry.name === '.sfs') continue - - const entryPath = path.join(dirPath, entry.name) - try { - const entryStat = await fsp.stat(entryPath) - const relativeKey = path.join(keyPrefix || '', entry.name).replace(/\\/g, '/') - - result.push({ - key: entry.isDirectory() ? `${relativeKey}/` : relativeKey, - size: entryStat.size, - lastModified: entryStat.mtime.toISOString(), - isDirectory: entry.isDirectory(), - }) - } catch { - // Skip entries we can't stat - } - } - - return { - bucket: bucket.name, - prefix: keyPrefix || '/', - entries: result, - hasMore: false, - total: result.length, - } + return this.getProvider(bucket).listDirectory(bucket, keyPrefix) } /** * List bucket contents with pagination. */ async listBucket(bucket: Bucket, limit: number = 100, cursor?: string): Promise { - const allEntries = await this.collectAllEntries(bucket, bucket.path, '') - - let startIdx = 0 - if (cursor) { - const cursorIdx = allEntries.findIndex(e => e.key === cursor) - if (cursorIdx >= 0) startIdx = cursorIdx + 1 - } - - const page = allEntries.slice(startIdx, startIdx + limit) - const hasMore = startIdx + limit < allEntries.length - const nextCursor = hasMore ? page[page.length - 1]?.key : undefined - - return { - bucket: bucket.name, - prefix: '/', - entries: page, - cursor: nextCursor, - hasMore, - total: allEntries.length, - } + return this.getProvider(bucket).listBucket(bucket, limit, cursor) } - private async collectAllEntries(bucket: Bucket, dirPath: string, prefix: string): Promise { - const result: ListEntry[] = [] - try { - const entries = await fsp.readdir(dirPath, { withFileTypes: true }) - for (const entry of entries) { - if (entry.name === '.sfs') continue - - const entryPath = path.join(dirPath, entry.name) - const relKey = prefix ? `${prefix}/${entry.name}` : entry.name - - try { - const stat = await fsp.stat(entryPath) - if (entry.isDirectory()) { - const subEntries = await this.collectAllEntries(bucket, entryPath, relKey) - result.push(...subEntries) - } else { - result.push({ - key: relKey, - size: stat.size, - lastModified: stat.mtime.toISOString(), - isDirectory: false, - }) - } - } catch { - // skip - } - } - } catch { - // empty - } - return result - } + // ── MIME helpers (independent of storage backend) ───────────────────────── /** * Detect MIME type using magic bytes, with fallback to extension. + * This method is only reliable for locally-accessible file paths. */ async detectMimeType(filePath: string): Promise { try { + const { fileTypeFromFile } = await import('file-type') const result = await fileTypeFromFile(filePath) if (result) return result.mime } catch { @@ -250,6 +136,9 @@ export class StorageService { return this.mimeFromExtension(filePath) } + /** + * Detect MIME type from an in-memory buffer using magic bytes. + */ async detectMimeTypeFromBuffer(buffer: Buffer): Promise { try { const result = await fileTypeFromBuffer(buffer) @@ -260,6 +149,14 @@ export class StorageService { return 'application/octet-stream' } + /** + * Returns true when the MIME type represents a raster image that can be + * processed by Sharp (excludes SVG). + */ + isImageMime(mimeType: string): boolean { + return mimeType.startsWith('image/') && mimeType !== 'image/svg+xml' + } + private mimeFromExtension(filePath: string): string { const ext = path.extname(filePath).toLowerCase() const mimeMap: Record = { @@ -286,13 +183,5 @@ export class StorageService { } return mimeMap[ext] ?? 'application/octet-stream' } - - isImageMime(mimeType: string): boolean { - return mimeType.startsWith('image/') && mimeType !== 'image/svg+xml' - } } - - - - diff --git a/extensions/entity-files/packages/simple-file-server/src/thumbnail/thumbnail.service.ts b/extensions/entity-files/packages/simple-file-server/src/thumbnail/thumbnail.service.ts index 0d715a4c..f2365149 100644 --- a/extensions/entity-files/packages/simple-file-server/src/thumbnail/thumbnail.service.ts +++ b/extensions/entity-files/packages/simple-file-server/src/thumbnail/thumbnail.service.ts @@ -34,6 +34,11 @@ export class ThumbnailService { * Stored: account1/200x200/producto.jpg */ async getThumbnail(bucket: Bucket, key: string, options: ThumbnailOptions): Promise { + // Thumbnails are only supported for local-storage buckets + if (bucket.storageTarget && bucket.storageTarget !== 'local') { + return null + } + const filePath = this.bucketService.resolveKey(bucket, key) // Check if original file exists diff --git a/extensions/entity-files/packages/simple-file-server/src/types/index.ts b/extensions/entity-files/packages/simple-file-server/src/types/index.ts index 76331c0f..ec636917 100644 --- a/extensions/entity-files/packages/simple-file-server/src/types/index.ts +++ b/extensions/entity-files/packages/simple-file-server/src/types/index.ts @@ -2,6 +2,8 @@ export type Permission = 'read' | 'write' | 'delete' +export type StorageTarget = 'local' | 'sftp' + export interface Grant { bucket: string prefixes: string[] @@ -16,10 +18,34 @@ export interface Identity { updatedAt: string } +/** + * SFTP connection configuration for a bucket backed by an SFTP server. + */ +export interface SftpConfig { + host: string + port: number + username: string + /** Plain-text password (mutually exclusive with privateKey) */ + password?: string + /** PEM-encoded private key content (mutually exclusive with password) */ + privateKey?: string + /** Passphrase for an encrypted private key */ + passphrase?: string +} + export interface Bucket { name: string + /** + * Absolute path to the storage root. + * - `local` target: absolute path on the local filesystem. + * - `sftp` target: absolute path on the remote SFTP server. + */ path: string createdAt: string + /** Storage backend for this bucket. Defaults to `'local'` when absent. */ + storageTarget?: StorageTarget + /** Required when storageTarget is `'sftp'`. */ + sftpConfig?: SftpConfig } export interface SFSConfig { diff --git a/extensions/entity-files/packages/simple-file-server/test/core.test.ts b/extensions/entity-files/packages/simple-file-server/test/core.test.ts index bfe9046b..46a1094d 100644 --- a/extensions/entity-files/packages/simple-file-server/test/core.test.ts +++ b/extensions/entity-files/packages/simple-file-server/test/core.test.ts @@ -4,10 +4,15 @@ import os from 'node:os' import fsp from 'node:fs/promises' import { BucketService } from '../src/storage/bucket.service.js' import { StorageService } from '../src/storage/storage.service.js' +import { StorageProviderRegistry } from '../src/storage/storage.provider.js' +import type { StorageProvider } from '../src/storage/storage.provider.js' import { ThumbnailService } from '../src/thumbnail/thumbnail.service.js' import { IdentityService } from '../src/auth/identity.service.js' import { ensureDataDirs } from '../src/config/config.service.js' import { Readable } from 'node:stream' +import type { Bucket, ListResult } from '../src/types/index.js' +import type { DownloadResult, UploadOptions } from '../src/storage/storage.provider.js' +import { SFSError, SFSErrorCode } from '../src/errors/index.js' let tempDir: string let bucketService: BucketService @@ -240,5 +245,157 @@ describe('ThumbnailService.buildThumbKey', () => { const result = await thumbnailService.getThumbnail(bucket, 'nonexistent.jpg', { width: 200, height: 200 }) expect(result).toBeNull() }) + + it('should return null for sftp buckets (thumbnails not supported)', async () => { + const sftpBucket: Bucket = { + name: 'sftp-thumb', + path: '/remote/files', + createdAt: new Date().toISOString(), + storageTarget: 'sftp', + sftpConfig: { host: 'localhost', port: 22, username: 'user', password: 'pass' }, + } + const result = await thumbnailService.getThumbnail(sftpBucket, 'image.jpg', { width: 200, height: 200 }) + expect(result).toBeNull() + }) +}) + +// ────────────────────────────────────────────────────────── +// StorageProviderRegistry Tests +// ────────────────────────────────────────────────────────── +describe('StorageProviderRegistry', () => { + it('should register and retrieve a provider', () => { + const registry = new StorageProviderRegistry() + const mockProvider = {} as StorageProvider + registry.register('mock', mockProvider) + expect(registry.get('mock')).toBe(mockProvider) + }) + + it('should throw for an unregistered target', () => { + const registry = new StorageProviderRegistry() + expect(() => registry.get('unknown')).toThrow(SFSError) + expect(() => registry.get('unknown')).toThrow('unknown') + }) + + it('should list all registered providers', () => { + const registry = new StorageProviderRegistry() + const p1 = {} as StorageProvider + const p2 = {} as StorageProvider + registry.register('a', p1) + registry.register('b', p2) + const all = registry.getAll() + expect(all.size).toBe(2) + expect(all.get('a')).toBe(p1) + expect(all.get('b')).toBe(p2) + }) + + it('should allow overwriting a registered provider', () => { + const registry = new StorageProviderRegistry() + const p1 = {} as StorageProvider + const p2 = {} as StorageProvider + registry.register('local', p1) + registry.register('local', p2) + expect(registry.get('local')).toBe(p2) + }) +}) + +// ────────────────────────────────────────────────────────── +// StorageService provider routing tests +// ────────────────────────────────────────────────────────── +describe('StorageService provider routing', () => { + it('should auto-register local provider', () => { + const registry = new StorageProviderRegistry() + const svc = new StorageService(bucketService, registry) + // Retrieving the local provider should not throw + expect(() => registry.get('local')).not.toThrow() + }) + + it('should allow registering a custom provider', () => { + const calls: string[] = [] + const customProvider: StorageProvider = { + async initBucket() { calls.push('init') }, + async validateBucket() { calls.push('validate') }, + async download() { return {} as DownloadResult }, + async upload() { return { size: 0, etag: '' } }, + async delete() {}, + async listDirectory() { return {} as ListResult }, + async listBucket() { return {} as ListResult }, + } + + storageService.registerProvider('custom', customProvider) + const bucket: Bucket = { + name: 'custom-bucket', + path: '/custom/path', + createdAt: new Date().toISOString(), + storageTarget: 'custom' as any, + } + + storageService.initBucket(bucket) + storageService.validateStartup().catch(() => {}) + expect(calls).toContain('init') + }) + + it('should use local provider for buckets without storageTarget', async () => { + const bucket = await bucketService.create('routing-test', path.join(tempDir, 'routing-test')) + expect(bucket.storageTarget).toBeUndefined() + // Local provider should handle this bucket + const content = 'routing test' + await storageService.upload(bucket, 'file.txt', Readable.from([content])) + const result = await storageService.download(bucket, 'file.txt') + const chunks: Buffer[] = [] + for await (const chunk of result.stream) { + chunks.push(Buffer.from(chunk)) + } + expect(Buffer.concat(chunks).toString()).toBe(content) + }) +}) + +// ────────────────────────────────────────────────────────── +// BucketService SFTP creation tests +// ────────────────────────────────────────────────────────── +describe('BucketService SFTP buckets', () => { + it('should create an sftp bucket with sftpConfig', async () => { + const bucket = await bucketService.create('sftp-test', '/remote/files', 'sftp', { + host: 'sftp.example.com', + port: 22, + username: 'deploy', + password: 'secret', + }) + expect(bucket.storageTarget).toBe('sftp') + expect(bucket.sftpConfig?.host).toBe('sftp.example.com') + expect(bucket.sftpConfig?.username).toBe('deploy') + expect(bucket.path).toBe('/remote/files') + }) + + it('should persist and reload sftp bucket metadata', async () => { + await bucketService.create('sftp-persist', '/remote/persist', 'sftp', { + host: 'sftp.example.com', + port: 2222, + username: 'user', + privateKey: '-----BEGIN RSA PRIVATE KEY-----\n...', + }) + const loaded = await bucketService.find('sftp-persist') + expect(loaded).not.toBeNull() + expect(loaded?.storageTarget).toBe('sftp') + expect(loaded?.sftpConfig?.port).toBe(2222) + expect(loaded?.sftpConfig?.privateKey).toBe('-----BEGIN RSA PRIVATE KEY-----\n...') + }) + + it('should reject sftp bucket creation without sftpConfig', async () => { + await expect(bucketService.create('sftp-bad', '/remote/path', 'sftp')).rejects.toThrow('sftpConfig is required') + }) + + it('should list both local and sftp buckets together', async () => { + await bucketService.create('local-one', path.join(tempDir, 'local-one')) + await bucketService.create('sftp-one', '/remote/sftp-one', 'sftp', { + host: 'host', + port: 22, + username: 'u', + password: 'p', + }) + const all = await bucketService.list() + const names = all.map(b => b.name) + expect(names).toContain('local-one') + expect(names).toContain('sftp-one') + }) })