diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4b479c..8baec13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + fastify@<5.7.2: '>=5.7.2' + fastify@<=5.7.2: '>=5.7.3' + undici@<6.23.0: '>=6.23.0' + importers: .: {} @@ -11,29 +16,29 @@ importers: server: dependencies: '@fastify/cors': - specifier: ^9.0.0 - version: 9.0.1 + specifier: ^11.2.0 + version: 11.2.0 '@fastify/static': - specifier: ^6.12.0 - version: 6.12.0 + specifier: ^9.0.0 + version: 9.0.0 fastify: - specifier: ^4.28.0 - version: 4.29.1 + specifier: ^5.7.4 + version: 5.7.4 undici: - specifier: ^6.21.0 - version: 6.22.0 + specifier: '>=6.23.0' + version: 7.22.0 zod: specifier: ^3.23.8 version: 3.25.76 devDependencies: '@types/node': - specifier: ^22.9.0 - version: 22.19.1 + specifier: ^25.2.3 + version: 25.2.3 tsx: - specifier: ^4.19.0 - version: 4.20.6 + specifier: ^4.21.0 + version: 4.21.0 typescript: - specifier: ^5.6.3 + specifier: ^5.9.3 version: 5.9.3 web: @@ -53,6 +58,12 @@ importers: tailwind-merge: specifier: ^3.4.0 version: 3.4.0 + uuid: + specifier: ^13.0.0 + version: 13.0.0 + zod: + specifier: ^3.25.76 + version: 3.25.76 devDependencies: '@eslint/js': specifier: ^9.39.1 @@ -71,7 +82,7 @@ importers: version: 19.2.3(@types/react@19.2.6) '@vitejs/plugin-react': specifier: ^5.1.1 - version: 5.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)) + version: 5.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) autoprefixer: specifier: ^10.4.24 version: 10.4.24(postcss@8.5.6) @@ -101,7 +112,7 @@ importers: version: 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6) + version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) packages: @@ -198,156 +209,312 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -386,30 +553,35 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@fastify/accept-negotiator@1.1.0': - resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} - engines: {node: '>=14'} + '@fastify/accept-negotiator@2.0.1': + resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} - '@fastify/ajv-compiler@3.6.0': - resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==} + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} - '@fastify/cors@9.0.1': - resolution: {integrity: sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==} + '@fastify/cors@11.2.0': + resolution: {integrity: sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==} - '@fastify/error@3.4.1': - resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} - '@fastify/fast-json-stringify-compiler@4.3.0': - resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} - '@fastify/merge-json-schemas@0.1.1': - resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==} + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} - '@fastify/send@2.1.0': - resolution: {integrity: sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==} + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} - '@fastify/static@6.12.0': - resolution: {integrity: sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==} + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + + '@fastify/send@4.1.0': + resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==} + + '@fastify/static@9.0.0': + resolution: {integrity: sha512-r64H8Woe/vfilg5RTy7lwWlE8ZZcTrc3kebYFMEUBrMqlydhQyoiExQXdYAy2REVpST/G35+stAM8WYp1WGmMA==} '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} @@ -427,6 +599,10 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -681,12 +857,12 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@22.19.1': - resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} - '@types/node@24.10.1': resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + '@types/node@25.2.3': + resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -773,14 +949,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - ajv-formats@2.1.1: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -813,12 +981,16 @@ packages: peerDependencies: postcss: ^8.1.0 - avvio@8.4.0: - resolution: {integrity: sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==} + avvio@9.2.0: + resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.2: + resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==} + engines: {node: 20 || >=22} + baseline-browser-mapping@2.8.30: resolution: {integrity: sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==} hasBin: true @@ -833,6 +1005,10 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.2: + resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} + engines: {node: 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -875,16 +1051,16 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} @@ -909,6 +1085,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -928,6 +1108,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -992,9 +1177,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - fast-content-type-parse@1.1.0: - resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} - fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} @@ -1008,8 +1190,8 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - fast-json-stringify@5.16.1: - resolution: {integrity: sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==} + fast-json-stringify@6.3.0: + resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} @@ -1017,17 +1199,14 @@ packages: fast-querystring@1.1.2: resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} - fast-uri@2.4.0: - resolution: {integrity: sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==} - fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fastify-plugin@4.5.1: - resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} + fastify-plugin@5.1.0: + resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} - fastify@4.29.1: - resolution: {integrity: sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==} + fastify@5.7.4: + resolution: {integrity: sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA==} fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -1049,9 +1228,9 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - find-my-way@8.2.2: - resolution: {integrity: sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==} - engines: {node: '>=14'} + find-my-way@9.4.0: + resolution: {integrity: sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w==} + engines: {node: '>=20'} find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} @@ -1064,16 +1243,9 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1094,10 +1266,9 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@8.1.0: - resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} - engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + glob@13.0.3: + resolution: {integrity: sha512-/g3B0mC+4x724v1TgtBlBtt2hPi/EWptsIAmXUx9Z2rvBYleQcsrmaOzd5LyL50jf/Soi83ZDJmw2+XqvH/EeA==} + engines: {node: 20 || >=22} globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} @@ -1143,16 +1314,12 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} @@ -1169,6 +1336,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -1188,8 +1359,8 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-schema-ref-resolver@1.0.1: - resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==} + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -1212,8 +1383,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - light-my-request@5.14.0: - resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==} + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} @@ -1292,6 +1463,10 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1316,19 +1491,20 @@ packages: engines: {node: '>=10.0.0'} hasBin: true + minimatch@10.2.0: + resolution: {integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} - minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - mnemonist@0.39.6: - resolution: {integrity: sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1344,16 +1520,10 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} - obliterator@2.0.5: - resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} - on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1378,6 +1548,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-scurry@2.0.1: + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} + engines: {node: 20 || >=22} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1389,14 +1563,14 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - pino-abstract-transport@2.0.0: - resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} pino-std-serializers@7.0.0: resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} - pino@9.14.0: - resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} hasBin: true postcss-value-parser@4.2.0: @@ -1410,16 +1584,12 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - process-warning@3.0.0: - resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1458,8 +1628,8 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - ret@0.4.3: - resolution: {integrity: sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==} + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} reusify@1.1.0: @@ -1477,11 +1647,8 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - safe-regex2@3.1.0: - resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==} + safe-regex2@5.0.0: + resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==} safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} @@ -1490,8 +1657,8 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - secure-json-parse@2.7.0: - resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} @@ -1549,8 +1716,9 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - thread-stream@3.1.0: - resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} @@ -1574,8 +1742,8 @@ packages: peerDependencies: typescript: '>=4.8.4' - tsx@4.20.6: - resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} hasBin: true @@ -1595,15 +1763,12 @@ packages: engines: {node: '>=14.17'} hasBin: true - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@6.22.0: - resolution: {integrity: sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==} - engines: {node: '>=18.17'} + undici@7.22.0: + resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} + engines: {node: '>=20.18.1'} update-browserslist-db@1.1.4: resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} @@ -1620,6 +1785,10 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + vite@7.2.4: resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1669,9 +1838,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -1807,81 +1973,159 @@ snapshots: '@esbuild/aix-ppc64@0.25.12': optional: true + '@esbuild/aix-ppc64@0.27.3': + optional: true + '@esbuild/android-arm64@0.25.12': optional: true + '@esbuild/android-arm64@0.27.3': + optional: true + '@esbuild/android-arm@0.25.12': optional: true + '@esbuild/android-arm@0.27.3': + optional: true + '@esbuild/android-x64@0.25.12': optional: true + '@esbuild/android-x64@0.27.3': + optional: true + '@esbuild/darwin-arm64@0.25.12': optional: true + '@esbuild/darwin-arm64@0.27.3': + optional: true + '@esbuild/darwin-x64@0.25.12': optional: true + '@esbuild/darwin-x64@0.27.3': + optional: true + '@esbuild/freebsd-arm64@0.25.12': optional: true + '@esbuild/freebsd-arm64@0.27.3': + optional: true + '@esbuild/freebsd-x64@0.25.12': optional: true + '@esbuild/freebsd-x64@0.27.3': + optional: true + '@esbuild/linux-arm64@0.25.12': optional: true + '@esbuild/linux-arm64@0.27.3': + optional: true + '@esbuild/linux-arm@0.25.12': optional: true + '@esbuild/linux-arm@0.27.3': + optional: true + '@esbuild/linux-ia32@0.25.12': optional: true + '@esbuild/linux-ia32@0.27.3': + optional: true + '@esbuild/linux-loong64@0.25.12': optional: true + '@esbuild/linux-loong64@0.27.3': + optional: true + '@esbuild/linux-mips64el@0.25.12': optional: true + '@esbuild/linux-mips64el@0.27.3': + optional: true + '@esbuild/linux-ppc64@0.25.12': optional: true + '@esbuild/linux-ppc64@0.27.3': + optional: true + '@esbuild/linux-riscv64@0.25.12': optional: true + '@esbuild/linux-riscv64@0.27.3': + optional: true + '@esbuild/linux-s390x@0.25.12': optional: true + '@esbuild/linux-s390x@0.27.3': + optional: true + '@esbuild/linux-x64@0.25.12': optional: true + '@esbuild/linux-x64@0.27.3': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true + '@esbuild/netbsd-arm64@0.27.3': + optional: true + '@esbuild/netbsd-x64@0.25.12': optional: true + '@esbuild/netbsd-x64@0.27.3': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true + '@esbuild/openbsd-arm64@0.27.3': + optional: true + '@esbuild/openbsd-x64@0.25.12': optional: true + '@esbuild/openbsd-x64@0.27.3': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true + '@esbuild/openharmony-arm64@0.27.3': + optional: true + '@esbuild/sunos-x64@0.25.12': optional: true + '@esbuild/sunos-x64@0.27.3': + optional: true + '@esbuild/win32-arm64@0.25.12': optional: true + '@esbuild/win32-arm64@0.27.3': + optional: true + '@esbuild/win32-ia32@0.25.12': optional: true + '@esbuild/win32-ia32@0.27.3': + optional: true + '@esbuild/win32-x64@0.25.12': optional: true + '@esbuild/win32-x64@0.27.3': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))': dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -1928,30 +2172,37 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 - '@fastify/accept-negotiator@1.1.0': {} + '@fastify/accept-negotiator@2.0.1': {} - '@fastify/ajv-compiler@3.6.0': + '@fastify/ajv-compiler@4.0.5': dependencies: ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - fast-uri: 2.4.0 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 - '@fastify/cors@9.0.1': + '@fastify/cors@11.2.0': dependencies: - fastify-plugin: 4.5.1 - mnemonist: 0.39.6 + fastify-plugin: 5.1.0 + toad-cache: 3.7.0 - '@fastify/error@3.4.1': {} + '@fastify/error@4.2.0': {} - '@fastify/fast-json-stringify-compiler@4.3.0': + '@fastify/fast-json-stringify-compiler@5.0.3': dependencies: - fast-json-stringify: 5.16.1 + fast-json-stringify: 6.3.0 - '@fastify/merge-json-schemas@0.1.1': + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': dependencies: - fast-deep-equal: 3.1.3 + dequal: 2.0.3 - '@fastify/send@2.1.0': + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.3.0 + + '@fastify/send@4.1.0': dependencies: '@lukeed/ms': 2.0.2 escape-html: 1.0.3 @@ -1959,14 +2210,14 @@ snapshots: http-errors: 2.0.0 mime: 3.0.0 - '@fastify/static@6.12.0': + '@fastify/static@9.0.0': dependencies: - '@fastify/accept-negotiator': 1.1.0 - '@fastify/send': 2.1.0 - content-disposition: 0.5.4 - fastify-plugin: 4.5.1 - glob: 8.1.0 - p-limit: 3.1.0 + '@fastify/accept-negotiator': 2.0.1 + '@fastify/send': 4.1.0 + content-disposition: 1.0.1 + fastify-plugin: 5.1.0 + fastq: 1.19.1 + glob: 13.0.3 '@humanfs/core@0.19.1': {} @@ -1979,6 +2230,8 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@isaacs/cliui@9.0.0': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2176,14 +2429,14 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@22.19.1': - dependencies: - undici-types: 6.21.0 - '@types/node@24.10.1': dependencies: undici-types: 7.16.0 + '@types/node@25.2.3': + dependencies: + undici-types: 7.16.0 + '@types/react-dom@19.2.3(@types/react@19.2.6)': dependencies: '@types/react': 19.2.6 @@ -2285,7 +2538,7 @@ snapshots: '@typescript-eslint/types': 8.47.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@5.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6))': + '@vitejs/plugin-react@5.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) @@ -2293,7 +2546,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.47 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6) + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -2305,10 +2558,6 @@ snapshots: acorn@8.15.0: {} - ajv-formats@2.1.1(ajv@8.17.1): - optionalDependencies: - ajv: 8.17.1 - ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -2344,13 +2593,17 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 - avvio@8.4.0: + avvio@9.2.0: dependencies: - '@fastify/error': 3.4.1 + '@fastify/error': 4.2.0 fastq: 1.19.1 balanced-match@1.0.2: {} + balanced-match@4.0.2: + dependencies: + jackspeak: 4.2.3 + baseline-browser-mapping@2.8.30: {} baseline-browser-mapping@2.9.19: {} @@ -2364,6 +2617,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.2: + dependencies: + balanced-match: 4.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -2405,13 +2662,11 @@ snapshots: concat-map@0.0.1: {} - content-disposition@0.5.4: - dependencies: - safe-buffer: 5.2.1 + content-disposition@1.0.1: {} convert-source-map@2.0.0: {} - cookie@0.7.2: {} + cookie@1.1.1: {} cross-spawn@7.0.6: dependencies: @@ -2429,6 +2684,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + detect-libc@2.1.2: {} electron-to-chromium@1.5.259: {} @@ -2469,6 +2726,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -2558,8 +2844,6 @@ snapshots: esutils@2.0.3: {} - fast-content-type-parse@1.1.0: {} - fast-decode-uri-component@1.0.1: {} fast-deep-equal@3.1.3: {} @@ -2574,14 +2858,13 @@ snapshots: fast-json-stable-stringify@2.1.0: {} - fast-json-stringify@5.16.1: + fast-json-stringify@6.3.0: dependencies: - '@fastify/merge-json-schemas': 0.1.1 + '@fastify/merge-json-schemas': 0.2.1 ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) - fast-deep-equal: 3.1.3 - fast-uri: 2.4.0 - json-schema-ref-resolver: 1.0.1 + fast-uri: 3.1.0 + json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 fast-levenshtein@2.0.6: {} @@ -2590,28 +2873,25 @@ snapshots: dependencies: fast-decode-uri-component: 1.0.1 - fast-uri@2.4.0: {} - fast-uri@3.1.0: {} - fastify-plugin@4.5.1: {} + fastify-plugin@5.1.0: {} - fastify@4.29.1: + fastify@5.7.4: dependencies: - '@fastify/ajv-compiler': 3.6.0 - '@fastify/error': 3.4.1 - '@fastify/fast-json-stringify-compiler': 4.3.0 + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 abstract-logging: 2.0.1 - avvio: 8.4.0 - fast-content-type-parse: 1.1.0 - fast-json-stringify: 5.16.1 - find-my-way: 8.2.2 - light-my-request: 5.14.0 - pino: 9.14.0 - process-warning: 3.0.0 - proxy-addr: 2.0.7 + avvio: 9.2.0 + fast-json-stringify: 6.3.0 + find-my-way: 9.4.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.0.0 rfdc: 1.4.1 - secure-json-parse: 2.7.0 + secure-json-parse: 4.1.0 semver: 7.7.3 toad-cache: 3.7.0 @@ -2631,11 +2911,11 @@ snapshots: dependencies: to-regex-range: 5.0.1 - find-my-way@8.2.2: + find-my-way@9.4.0: dependencies: fast-deep-equal: 3.1.3 fast-querystring: 1.1.2 - safe-regex2: 3.1.0 + safe-regex2: 5.0.0 find-up@5.0.0: dependencies: @@ -2649,12 +2929,8 @@ snapshots: flatted@3.3.3: {} - forwarded@0.2.0: {} - fraction.js@5.3.4: {} - fs.realpath@1.0.0: {} - fsevents@2.3.3: optional: true @@ -2672,13 +2948,11 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@8.1.0: + glob@13.0.3: dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 5.1.6 - once: 1.4.0 + minimatch: 10.2.0 + minipass: 7.1.2 + path-scurry: 2.0.1 globals@14.0.0: {} @@ -2715,14 +2989,9 @@ snapshots: imurmurhash@0.1.4: {} - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - inherits@2.0.4: {} - ipaddr.js@1.9.1: {} + ipaddr.js@2.3.0: {} is-extglob@2.1.1: {} @@ -2734,6 +3003,10 @@ snapshots: isexe@2.0.0: {} + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + jiti@2.6.1: {} js-tokens@4.0.0: {} @@ -2746,9 +3019,9 @@ snapshots: json-buffer@3.0.1: {} - json-schema-ref-resolver@1.0.1: + json-schema-ref-resolver@3.0.0: dependencies: - fast-deep-equal: 3.1.3 + dequal: 2.0.3 json-schema-traverse@0.4.1: {} @@ -2767,10 +3040,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - light-my-request@5.14.0: + light-my-request@6.6.0: dependencies: - cookie: 0.7.2 - process-warning: 3.0.0 + cookie: 1.1.1 + process-warning: 4.0.1 set-cookie-parser: 2.7.2 lightningcss-android-arm64@1.30.2: @@ -2828,6 +3101,8 @@ snapshots: lodash.merge@4.6.2: {} + lru-cache@11.2.6: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -2849,21 +3124,19 @@ snapshots: mime@3.0.0: {} + minimatch@10.2.0: + dependencies: + brace-expansion: 5.0.2 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 - minimatch@5.1.6: - dependencies: - brace-expansion: 2.0.2 - minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 - mnemonist@0.39.6: - dependencies: - obliterator: 2.0.5 + minipass@7.1.2: {} ms@2.1.3: {} @@ -2873,14 +3146,8 @@ snapshots: node-releases@2.0.27: {} - obliterator@2.0.5: {} - on-exit-leak-free@2.1.2: {} - once@1.4.0: - dependencies: - wrappy: 1.0.2 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2906,31 +3173,36 @@ snapshots: path-key@3.1.1: {} + path-scurry@2.0.1: + dependencies: + lru-cache: 11.2.6 + minipass: 7.1.2 + picocolors@1.1.1: {} picomatch@2.3.1: {} picomatch@4.0.3: {} - pino-abstract-transport@2.0.0: + pino-abstract-transport@3.0.0: dependencies: split2: 4.2.0 pino-std-serializers@7.0.0: {} - pino@9.14.0: + pino@10.3.1: dependencies: '@pinojs/redact': 0.4.0 atomic-sleep: 1.0.0 on-exit-leak-free: 2.1.2 - pino-abstract-transport: 2.0.0 + pino-abstract-transport: 3.0.0 pino-std-serializers: 7.0.0 process-warning: 5.0.0 quick-format-unescaped: 4.0.4 real-require: 0.2.0 safe-stable-stringify: 2.5.0 sonic-boom: 4.2.0 - thread-stream: 3.1.0 + thread-stream: 4.0.0 postcss-value-parser@4.2.0: {} @@ -2942,15 +3214,10 @@ snapshots: prelude-ls@1.2.1: {} - process-warning@3.0.0: {} + process-warning@4.0.1: {} process-warning@5.0.0: {} - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -2974,7 +3241,7 @@ snapshots: resolve-pkg-maps@1.0.0: {} - ret@0.4.3: {} + ret@0.5.0: {} reusify@1.1.0: {} @@ -3012,17 +3279,15 @@ snapshots: dependencies: queue-microtask: 1.2.3 - safe-buffer@5.2.1: {} - - safe-regex2@3.1.0: + safe-regex2@5.0.0: dependencies: - ret: 0.4.3 + ret: 0.5.0 safe-stable-stringify@2.5.0: {} scheduler@0.27.0: {} - secure-json-parse@2.7.0: {} + secure-json-parse@4.1.0: {} semver@6.3.1: {} @@ -3060,7 +3325,7 @@ snapshots: tapable@2.3.0: {} - thread-stream@3.1.0: + thread-stream@4.0.0: dependencies: real-require: 0.2.0 @@ -3081,9 +3346,9 @@ snapshots: dependencies: typescript: 5.9.3 - tsx@4.20.6: + tsx@4.21.0: dependencies: - esbuild: 0.25.12 + esbuild: 0.27.3 get-tsconfig: 4.13.0 optionalDependencies: fsevents: 2.3.3 @@ -3105,11 +3370,9 @@ snapshots: typescript@5.9.3: {} - undici-types@6.21.0: {} - undici-types@7.16.0: {} - undici@6.22.0: {} + undici@7.22.0: {} update-browserslist-db@1.1.4(browserslist@4.28.0): dependencies: @@ -3127,7 +3390,9 @@ snapshots: dependencies: punycode: 2.3.1 - vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6): + uuid@13.0.0: {} + + vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -3140,7 +3405,7 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 - tsx: 4.20.6 + tsx: 4.21.0 which@2.0.2: dependencies: @@ -3148,8 +3413,6 @@ snapshots: word-wrap@1.2.5: {} - wrappy@1.0.2: {} - yallist@3.1.1: {} yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3e1aab4..d8475cd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,3 +4,8 @@ packages: onlyBuiltDependencies: - esbuild + +overrides: + fastify@<5.7.2: '>=5.7.2' + fastify@<=5.7.2: '>=5.7.3' + undici@<6.23.0: '>=6.23.0' diff --git a/server/package.json b/server/package.json index 65c9c6d..3eda9cf 100644 --- a/server/package.json +++ b/server/package.json @@ -10,15 +10,15 @@ "start": "node dist/index.js" }, "dependencies": { - "fastify": "^4.28.0", - "@fastify/cors": "^9.0.0", - "@fastify/static": "^6.12.0", + "@fastify/cors": "^11.2.0", + "@fastify/static": "^9.0.0", + "fastify": "^5.7.4", "undici": "^6.21.0", "zod": "^3.23.8" }, "devDependencies": { - "@types/node": "^22.9.0", - "tsx": "^4.19.0", - "typescript": "^5.6.3" + "@types/node": "^25.2.3", + "tsx": "^4.21.0", + "typescript": "^5.9.3" } } diff --git a/server/src/index.ts b/server/src/index.ts index 9f9d79b..f26162c 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,10 +1,12 @@ -import Fastify from "fastify"; +import Fastify, { FastifyRequest, FastifyReply } from "fastify"; import cors from "@fastify/cors"; import fastifyStatic from "@fastify/static"; import { z } from "zod"; -import { Agent, setGlobalDispatcher, request } from "undici"; +import { Agent, setGlobalDispatcher } from "undici"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; +import { sessionManager } from "./lib/SessionManager"; +import { DownloadManager } from "./lib/DownloadManager"; // Otimização: Agente HTTP global com conexões persistentes e timeout relaxado. // Isso ajuda a manter a performance em downloads paralelos e evita gargalos de socket. @@ -59,112 +61,78 @@ const streamQuerySchema = z.object({ .optional(), }); -app.get("/stream", async (requestFastify, reply) => { - const parseResult = streamQuerySchema.safeParse(requestFastify.query); +const downloadManager = new DownloadManager(agent); - if (!parseResult.success) { - const firstError = parseResult.error.errors[0]?.message ?? "Parâmetros inválidos"; - reply.status(400); - return { error: firstError }; - } +// Helper to get session from header +const getSession = (req: FastifyRequest, reply: FastifyReply) => { + const authHeader = req.headers["x-session-id"]; + const id = Array.isArray(authHeader) ? authHeader[0] : authHeader; + const session = sessionManager.getSession(id); - const { url, offset } = parseResult.data; + // Return the ID for the client to store if it's new + reply.header("x-session-id", session.id); + return session; +}; + +// 1. Session Status & Timer +app.get("/api/session", async (request, reply) => { + const session = getSession(request, reply); + return { + sessionId: session.id, + expiresAt: sessionManager.getExpiresAt(session.id), + remainingMs: sessionManager.getSessionTimeRemaining(session.id), + }; +}); + +// 2. Queue Download +const queueSchema = z.object({ + url: z.string().url(), +}); + +app.post("/api/queue", async (request, reply) => { + const session = getSession(request, reply); + + const parse = queueSchema.safeParse(request.body); + if (!parse.success) return reply.status(400).send({ error: "Invalid URL" }); + + const task = await downloadManager.queueDownload(session.id, parse.data.url); + return task; +}); + +// 3. List Tasks (Polling) +app.get("/api/tasks", async (request, reply) => { + const session = getSession(request, reply); + const tasks = downloadManager.getTasks(session.id); + return tasks; +}); + +// 4. Download Completed File (Local -> User) +app.get("/api/download/:taskId", async (request, reply) => { + const session = getSession(request, reply); + const { taskId } = request.params as { taskId: string }; + + const task = downloadManager.getTask(taskId); + + if (!task) return reply.status(404).send({ error: "Task not found" }); + if (task.sessionId !== session.id) return reply.status(403).send({ error: "Unauthorized" }); + if (task.status !== "completed") return reply.status(400).send({ error: "Download not ready" }); + + const sessionPath = sessionManager.getSessionPath(session.id); + const filePath = resolve(sessionPath, `${task.id}_${task.filename}`); try { - // HEAD para validar link e obter metadados. - const headResponse = await request(url, { - method: "HEAD", - headersTimeout: 15000, - headers: { - "User-Agent": "Ephemeral-Downloader/1.0", - }, - dispatcher: agent, - }); - - if (headResponse.statusCode && headResponse.statusCode >= 400) { - reply.status(502); - return { error: `Erro na origem: ${headResponse.statusCode}` }; - } - - const contentType = - headResponse.headers["content-type"] ?? "application/octet-stream"; - const contentLength = headResponse.headers["content-length"]; - const originDisposition = headResponse.headers["content-disposition"]; - - // Nome de arquivo fallback. - const fallbackName = (() => { - try { - const parsedUrl = new URL(url); - const nameFromPath = parsedUrl.pathname.split("/").filter(Boolean).pop(); - return nameFromPath ?? "download.bin"; - } catch { - return "download.bin"; - } - })(); - - const filename = (() => { - if (typeof originDisposition === "string") { - const match = /filename\*?=(?:UTF-8''|")?([^";]+)/i.exec(originDisposition); - if (match?.[1]) { - return decodeURIComponent(match[1].replace(/"/g, "")); - } - } - return fallbackName; - })(); - - const headers: Record = { - "User-Agent": "Ephemeral-Downloader/1.0", - }; - - if (typeof offset === "number" && Number.isFinite(offset) && offset > 0) { - headers.Range = `bytes=${offset}-`; - } - - // GET streaming da origem. - const originResponse = await request(url, { - method: "GET", - headers, - headersTimeout: 30000, - maxRedirections: 5, - dispatcher: agent, - }); - - if (!originResponse.body) { - reply.status(502); - return { error: "Sem corpo de resposta na origem." }; - } - - if (originResponse.statusCode && originResponse.statusCode >= 400) { - reply.status(originResponse.statusCode); - return { error: "Falha ao baixar arquivo da origem." }; - } - - const normalizedLength = Array.isArray(contentLength) - ? contentLength[0] - : contentLength; - - // Configura headers de resposta. - reply.headers({ - "Content-Type": Array.isArray(contentType) ? contentType[0] : contentType, - ...(normalizedLength - ? { - "Content-Length": normalizedLength, - "X-Origin-Content-Length": normalizedLength, - } - : {}), - "Content-Disposition": `attachment; filename="${filename}"`, - }); - - // Pipe direto. - return reply.send(originResponse.body); - } catch (error) { - requestFastify.log.error({ err: error, url }, "Erro no proxy"); - if (!reply.sent) { - return reply.status(502).send({ error: "Erro interno no proxy." }); - } + sessionManager.touch(session.id); // Reset timer on interaction + return reply.download(filePath, task.filename); // requires @fastify/static or sendFile + } catch (e) { + return reply.status(404).send({ error: "File removed or missing" }); } }); +// Legacy Stream (Redirect or Deprecate) +app.get("/stream", async (req, reply) => { + reply.status(410).send({ error: "Direct streaming is deprecated. Use the new Dashboard." }); +}); + // Fallback para SPA. app.setNotFoundHandler((requestFastify, reply) => { if (requestFastify.method === "GET") { diff --git a/server/src/lib/DownloadManager.ts b/server/src/lib/DownloadManager.ts new file mode 100644 index 0000000..875e792 --- /dev/null +++ b/server/src/lib/DownloadManager.ts @@ -0,0 +1,147 @@ +import { createWriteStream, existsSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { request, Dispatcher } from "undici"; +import { randomUUID } from "node:crypto"; +import { sessionManager } from "./SessionManager"; + +type DownloadStatus = "queued" | "downloading" | "completed" | "error"; + +export type DownloadTask = { + id: string; + sessionId: string; + url: string; + filename: string; + status: DownloadStatus; + progress: number; + totalBytes: number; + receivedBytes: number; + speed: number; + error?: string; + createdAt: number; +}; + +// Map to store active task metadata in memory (for speed) +// Ideally this would be persisted to JSON in the session folder for crash recovery +const tasks = new Map(); +const agent = new Dispatcher(); // Should use the global agent from index.ts ideally, or pass it in. + +export class DownloadManager { + + constructor(private globalAgent: Dispatcher) { } + + public getTasks(sessionId: string): DownloadTask[] { + return Array.from(tasks.values()).filter(t => t.sessionId === sessionId); + } + + public getTask(taskId: string): DownloadTask | undefined { + return tasks.get(taskId); + } + + public async queueDownload(sessionId: string, url: string): Promise { + const id = randomUUID(); + + // Try to determine filename + let filename = "download.bin"; + try { + const u = new URL(url); + const p = u.pathname.split("/").pop(); + if (p) filename = p; + } catch { } + + const task: DownloadTask = { + id, + sessionId, + url, + filename, // Will be updated after HEAD request if possible + status: "queued", + progress: 0, + totalBytes: 0, + receivedBytes: 0, + speed: 0, + createdAt: Date.now() + }; + + tasks.set(id, task); + this.startDownload(task); + + return task; + } + + private async startDownload(task: DownloadTask) { + const sessionPath = sessionManager.getSessionPath(task.sessionId); + if (!existsSync(sessionPath)) { + task.status = "error"; + task.error = "Session expired or missing"; + return; + } + + task.status = "downloading"; + + try { + // Step 1: HEAD/GET to stream + const response = await request(task.url, { + dispatcher: this.globalAgent, + headersTimeout: 15000, + headers: { "User-Agent": "Ephemeral-Downloader/2.0" } + }); + + if (response.statusCode >= 400) { + throw new Error(`HTTP Error ${response.statusCode}`); + } + + // Update filename from headers if available + const disposition = response.headers["content-disposition"]; + if (typeof disposition === "string") { + const match = /filename\*?=(?:UTF-8''|")?([^";]+)/i.exec(disposition); + if (match?.[1]) { + task.filename = decodeURIComponent(match[1].replace(/"/g, "")); + } + } + + // Size + const len = response.headers["content-length"]; + if (len) { + task.totalBytes = Number(Array.isArray(len) ? len[0] : len); + } + + const filePath = join(sessionPath, `${task.id}_${task.filename}`); + const fileStream = createWriteStream(filePath); + + let lastUpdate = Date.now(); + let bytesAtLastUpdate = 0; + + for await (const chunk of response.body) { + fileStream.write(chunk); + task.receivedBytes += chunk.length; + + const now = Date.now(); + if (now - lastUpdate > 500) { // Update speed every 500ms + const diff = now - lastUpdate; + const bytesDiff = task.receivedBytes - bytesAtLastUpdate; + task.speed = (bytesDiff / diff) * 1000; + + if (task.totalBytes > 0) { + task.progress = Math.round((task.receivedBytes / task.totalBytes) * 100); + } + + lastUpdate = now; + bytesAtLastUpdate = task.receivedBytes; + + // Touch session to keep it alive during download + sessionManager.touch(task.sessionId); + } + } + + fileStream.end(); + task.status = "completed"; + task.progress = 100; + task.speed = 0; + sessionManager.touch(task.sessionId); // keep alive after finish + + } catch (err) { + task.status = "error"; + task.error = err instanceof Error ? err.message : "Unknown error"; + console.error(`[DownloadManager] Download failed: ${task.url}`, err); + } + } +} diff --git a/server/src/lib/SessionManager.ts b/server/src/lib/SessionManager.ts new file mode 100644 index 0000000..7ad1756 --- /dev/null +++ b/server/src/lib/SessionManager.ts @@ -0,0 +1,134 @@ +import { existsSync, mkdirSync, rmSync, readdirSync, statSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { randomUUID } from "node:crypto"; + +const STORAGE_ROOT = resolve(process.cwd(), "server/storage"); +const SESSION_TIMEOUT_MS = 60 * 60 * 1000; // 1 Hour + +type SessionData = { + id: string; + lastActive: number; +}; + +export class SessionManager { + private sessions: Map = new Map(); + + constructor() { + // Ensure storage root exists + if (!existsSync(STORAGE_ROOT)) { + mkdirSync(STORAGE_ROOT, { recursive: true }); + } + + // Start Garbage Collector + setInterval(() => this.cleanup(), 60 * 1000); // Check every minute + console.log("[SessionManager] Garbage Collector started."); + } + + /** + * Gets or creates a session. Updates lastActive timestamp. + */ + public getSession(id?: string): SessionData { + const now = Date.now(); + + if (id && this.sessions.has(id)) { + const session = this.sessions.get(id)!; + session.lastActive = now; + return session; + } + + // If ID provided but not in memory, check if folder exists (recovery) + if (id && existsSync(join(STORAGE_ROOT, id))) { + const session = { id, lastActive: now }; + this.sessions.set(id, session); + return session; + } + + // Create new session + const newId = randomUUID(); + const session = { id: newId, lastActive: now }; + this.sessions.set(newId, session); + + // Create folder + const sessionPath = this.getSessionPath(newId); + if (!existsSync(sessionPath)) { + mkdirSync(sessionPath, { recursive: true }); + } + + return session; + } + + public getSessionPath(sessionId: string): string { + return join(STORAGE_ROOT, sessionId); + } + + public getSessionTimeRemaining(sessionId: string): number { + const session = this.sessions.get(sessionId); + if (!session) return 0; + const expiresAt = session.lastActive + SESSION_TIMEOUT_MS; + return Math.max(0, expiresAt - Date.now()); + } + + public getExpiresAt(sessionId: string): string { + const session = this.sessions.get(sessionId); + if (!session) return new Date().toISOString(); + return new Date(session.lastActive + SESSION_TIMEOUT_MS).toISOString(); + } + + public touch(sessionId: string) { + if (this.sessions.has(sessionId)) { + this.sessions.get(sessionId)!.lastActive = Date.now(); + } + } + + /** + * Removes expired sessions and deletes their files. + */ + private cleanup() { + const now = Date.now(); + + // Check in-memory sessions + for (const [id, session] of this.sessions.entries()) { + if (now - session.lastActive > SESSION_TIMEOUT_MS) { + console.log(`[SessionManager] Session ${id} expired. Cleaning up...`); + this.destroySession(id); + } + } + + // Double check orphaned folders (in case server restarted) + // accessible folders in storage that are not in memory OR are too old + try { + const dirs = readdirSync(STORAGE_ROOT); + for (const dir of dirs) { + if (dir === ".gitignore") continue; + + const dirPath = join(STORAGE_ROOT, dir); + const stats = statSync(dirPath); + + // If folder is older than timeout and not in active memory (or in memory but expired) + // Note: mtime might update when files are written, so this is a heuristic fallback + const isInMemory = this.sessions.has(dir); + if (!isInMemory && (now - stats.mtimeMs > SESSION_TIMEOUT_MS)) { + console.log(`[SessionManager] Removing orphaned session folder: ${dir}`); + rmSync(dirPath, { recursive: true, force: true }); + } + } + } catch (err) { + console.error("[SessionManager] Error during fallback cleanup", err); + } + } + + private destroySession(id: string) { + this.sessions.delete(id); + const path = this.getSessionPath(id); + if (existsSync(path)) { + try { + rmSync(path, { recursive: true, force: true }); + console.log(`[SessionManager] Deleted storage for ${id}`); + } catch (e) { + console.error(`[SessionManager] Failed to delete ${path}`, e); + } + } + } +} + +export const sessionManager = new SessionManager(); diff --git a/web/package.json b/web/package.json index 6f04cae..f50a571 100644 --- a/web/package.json +++ b/web/package.json @@ -14,7 +14,9 @@ "lucide-react": "^0.564.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "tailwind-merge": "^3.4.0" + "tailwind-merge": "^3.4.0", + "uuid": "^13.0.0", + "zod": "^3.25.76" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/web/src/App.tsx b/web/src/App.tsx index 2ea9b62..26d7981 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,230 +1,91 @@ -import { useState } from "react"; -import { Layout } from "./components/ui/Layout"; +import "./App.css"; import { DownloadForm } from "./components/feature/DownloadForm"; -import { DownloadItem, type DownloadItemData } from "./components/feature/DownloadItem"; -import { Card, CardContent, CardHeader, CardTitle } from "./components/ui/Card"; +import { DownloadItem } from "./components/feature/DownloadItem"; +import { Layout } from "./components/ui/Layout"; +import { Alert, AlertDescription, AlertTitle } from "./components/ui/Alert"; +import { AlertCircle } from "lucide-react"; +import { SessionProvider, useSession } from "./context/SessionContext"; +import { SessionTimer } from "./components/feature/SessionTimer"; -const API_URL = import.meta.env.VITE_API_URL ?? "http://localhost:3000"; +function DownloadList() { + const { tasks, expiresAt } = useSession(); -function App() { - const [downloads, setDownloads] = useState([]); + return ( +
+
+

Active Downloads

+ +
- const startDownload = async (itemId: number, url: string, resume = false) => { - const existing = downloads.find((item) => item.id === itemId); - const alreadyReceived = resume && existing?.receivedBytes - ? existing.receivedBytes - : 0; + {tasks.length === 0 ? ( +
+

No downloads in session

+

Files are removed after 1 hour of inactivity

+
+ ) : ( +
+ {tasks.map((task) => ( + + ))} +
+ )} +
+ ); +} - setDownloads((current) => - current.map((item) => - item.id === itemId - ? { - ...item, - status: "downloading", - progress: item.progress ?? 0, - error: undefined, - speedHistory: [], // Reset history on start/resume - } - : item, - ), - ); +function Main() { + const { queueDownload } = useSession(); + const handleDownload = async (url: string) => { try { - const query = new URLSearchParams({ - url, - }); - if (resume && alreadyReceived > 0) { - query.set("offset", String(alreadyReceived)); - } - - const response = await fetch( - `${API_URL}/stream?${query.toString()}`, - ); - - if (!response.ok || !response.body) { - const message = "Failed to start download."; - setDownloads((current) => - current.map((item) => - item.id === itemId - ? { ...item, status: "error", error: message, progress: null } - : item, - ), - ); - return; - } - - const contentLengthHeader = - response.headers.get("x-origin-content-length") ?? - response.headers.get("content-length"); - - const totalBytes = contentLengthHeader - ? Number.parseInt(contentLengthHeader, 10) - : 0; - - const reader = response.body.getReader(); - const chunks: BlobPart[] = []; - let received = alreadyReceived; - const startedAt = performance.now(); - - // eslint-disable-next-line no-constant-condition - while (true) { - const { done, value } = await reader.read(); - if (done) break; - if (value) { - chunks.push(value.buffer as ArrayBuffer); - received += value.length; - const elapsedSeconds = (performance.now() - startedAt) / 1000; - const bytesPerSecond = - elapsedSeconds > 0 ? received / elapsedSeconds : 0; - const percentage = - totalBytes > 0 ? Math.round((received / totalBytes) * 100) : null; - - setDownloads((current) => - current.map((item) => { - if (item.id !== itemId) return item; - - const newHistory = [...(item.speedHistory || []), bytesPerSecond]; - // Keep last 50 data points for the graph - if (newHistory.length > 50) newHistory.shift(); - - return { - ...item, - progress: percentage, - receivedBytes: received, - totalBytes: totalBytes || item.totalBytes, - speedBytesPerSecond: bytesPerSecond, - speedHistory: newHistory, - }; - }), - ); - } - } - - const contentType = - response.headers.get("content-type") ?? "application/octet-stream"; - const blob = new Blob(chunks, { type: contentType }); - - const disposition = response.headers.get("content-disposition"); - const fallbackName = (() => { - try { - const parsed = new URL(url); - const nameFromPath = parsed.pathname.split("/").filter(Boolean).pop(); - return nameFromPath ?? "download.bin"; - } catch { - return "download.bin"; - } - })(); - - const filename = (() => { - if (disposition) { - const match = - /filename\*?=(?:UTF-8''|")?([^";]+)/i.exec(disposition); - if (match?.[1]) { - return decodeURIComponent(match[1].replace(/"/g, "")); - } - } - return fallbackName; - })(); - - const blobUrl = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = blobUrl; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(blobUrl); - - setDownloads((current) => - current.map((item) => - item.id === itemId - ? { ...item, status: "completed", progress: 100 } - : item, - ), - ); - } catch (err) { - const message = - err instanceof Error ? err.message : "Unexpected error during download."; - setDownloads((current) => - current.map((item) => - item.id === itemId - ? { ...item, status: "error", error: message, progress: null } - : item, - ), - ); + await queueDownload(url); + } catch (error) { + console.error("Failed to queue", error); } }; - const handleNewDownloads = (urls: string[]) => { - const baseId = Date.now(); - const newItems: DownloadItemData[] = urls.map((value, index) => ({ - id: baseId + index, - url: value, - status: "pending", - progress: 0, - speedHistory: [], - })); - - setDownloads((current) => [...newItems, ...current]); - - newItems.forEach((item) => { - void startDownload(item.id, item.url); - }); - }; - - const handleRetry = (id: number, url: string) => { - void startDownload(id, url, true); - }; - return ( -
-
- - - - - Tips - - -

• Use direct links ending in .iso, .zip, .mp4, etc.

-

• Downloads are streamed directly to your browser memory, then saved.

-

• If the tab closes, the download is lost.

-
-
-
- -
-
-

Active Downloads

- {downloads.length} items -
- -
- {downloads.length === 0 ? ( -
-
-
-
-

No downloads yet

-

- Paste your links on the left sidebar to get started. The VPS proxy will handle the connection. +

+ {/* Left Column: Input Form */} +
+
+
+
+

New Download

+

+ Paste a link to start. Files are downloaded to the server first.

- ) : ( - downloads.map((item) => ( - - )) - )} + +
+ + + + Ephemeral Session + + Files are stored for 1 hour. The timer resets on interaction. + After expiration, all files are permanently deleted. + +
-
+
+ + {/* Right Column: Downloads List */} +
+ +
); } +function App() { + return ( + +
+ + ); +} + export default App; diff --git a/web/src/components/feature/DownloadForm.tsx b/web/src/components/feature/DownloadForm.tsx index 62d95f4..f5e1f05 100644 --- a/web/src/components/feature/DownloadForm.tsx +++ b/web/src/components/feature/DownloadForm.tsx @@ -1,75 +1,54 @@ import { useState } from "react"; -import type { FormEvent } from "react"; -import { Download } from "lucide-react"; import { Button } from "../ui/Button"; -import { Textarea } from "../ui/Textarea"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/Card"; +import { Input } from "../ui/Input"; +import { Download } from "lucide-react"; +import { z } from "zod"; + +const formSchema = z.object({ + url: z.string().url("Please enter a valid URL"), +}); interface DownloadFormProps { - onSubmit: (urls: string[]) => void; + onDownload: (url: string) => void; } -export function DownloadForm({ onSubmit }: DownloadFormProps) { - const [text, setText] = useState(""); +export function DownloadForm({ onDownload }: DownloadFormProps) { + const [url, setUrl] = useState(""); const [error, setError] = useState(null); - const handleSubmit = (e: FormEvent) => { + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setError(null); - const urls = text - .split(/\r?\n/) - .map((u) => u.trim()) - .filter(Boolean); + const result = formSchema.safeParse({ url }); - if (urls.length === 0) { - setError("Please enter at least one URL."); + if (!result.success) { + setError(result.error.errors[0].message); return; } - const invalidUrl = urls.find((u) => { - try { - new URL(u); - return false; - } catch { - return true; - } - }); - - if (invalidUrl) { - setError(`Invalid URL detected: ${invalidUrl}`); - return; - } - - onSubmit(urls); - setText(""); + onDownload(url); + setUrl(""); }; return ( - - - New Download - - Enter direct links (HTTP/HTTPS) to stream via VPS. One URL per line. - - - -
-