diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1bc3b17 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +.git +.gitignore +README.md +LICENSE +Dockerfile +.dockerignore + +# Go +vendor/ +software-station +*.out +*.test +coverage.out + +# Frontend +node_modules/ +frontend/node_modules/ +frontend/build/ +frontend/.svelte-kit/ + +# Application Data +.cache/ +hashes.json +test_software.txt +test_hashes.json +test_updater.txt + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b05676a --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Binaries +software-station +software-station.exe + +# Go +vendor/ +*.out +*.test +coverage.out + +# Frontend +node_modules/ +frontend/node_modules/ +frontend/build/ +frontend/.svelte-kit/ +frontend/.env +.env +.env.* + +# Application Data +.cache/ +hashes.json +test_software.txt +test_hashes.json +test_updater.txt + +# OS +.DS_Store +Thumbs.db + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f44c2b9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 Quad4 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e8f1c05 --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ +.PHONY: all build-frontend build-go clean release run lint scan check format test dev + +BINARY_NAME=software-station +FRONTEND_DIR=frontend +BUILD_DIR=build + +all: build-frontend build-go + +dev: + @echo "Starting development environment..." + @pnpm --prefix $(FRONTEND_DIR) dev & go run main.go + +build-frontend: + cd $(FRONTEND_DIR) && pnpm install && pnpm build + +build-go: + go build -o $(BINARY_NAME) main.go + +release: build-frontend + CGO_ENABLED=0 go build -ldflags="-s -w" -o $(BINARY_NAME) main.go + @echo "Release build complete: $(BINARY_NAME)" + +run: all + ./$(BINARY_NAME) + +format: + go fmt ./... + cd $(FRONTEND_DIR) && pnpm run format + +lint: + go vet ./... + cd $(FRONTEND_DIR) && pnpm run lint + +scan: + gosec ./... + +check: + cd $(FRONTEND_DIR) && pnpm run check + +test: + go test -v -coverpkg=./... ./... + +clean: + rm -rf $(FRONTEND_DIR)/build + rm -f $(BINARY_NAME) + rm -f coverage.out diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..49faeab --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,6 @@ +node_modules +.svelte-kit +build +dist +.DS_Store + diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..5b91da1 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,8 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..75842c4 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..0c62593 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,59 @@ +import js from '@eslint/js'; +import tsPlugin from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import sveltePlugin from 'eslint-plugin-svelte'; +import svelteParser from 'svelte-eslint-parser'; + +export default [ + js.configs.recommended, + { + files: ['**/*.{js,mjs,cjs,ts,svelte}'], + languageOptions: { + parser: tsParser, + parserOptions: { + sourceType: 'module', + ecmaVersion: 2020, + }, + globals: { + fetch: 'readonly', + console: 'readonly', + window: 'readonly', + document: 'readonly', + localStorage: 'readonly', + $state: 'readonly', + $derived: 'readonly', + $effect: 'readonly', + $props: 'readonly', + $inspect: 'readonly', + $host: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tsPlugin, + svelte: sveltePlugin, + }, + rules: { + ...tsPlugin.configs.recommended.rules, + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'off', + }, + }, + { + files: ['**/*.svelte'], + languageOptions: { + parser: svelteParser, + parserOptions: { + parser: tsParser, + }, + }, + plugins: { + svelte: sveltePlugin, + }, + rules: { + ...sveltePlugin.configs.recommended.rules, + }, + }, + { + ignores: ['node_modules/**', '.svelte-kit/**', 'build/**', 'dist/**'], + }, +]; diff --git a/frontend/frontend/static/icons/gitea-dark.webp b/frontend/frontend/static/icons/gitea-dark.webp new file mode 100644 index 0000000..7b4ebb5 Binary files /dev/null and b/frontend/frontend/static/icons/gitea-dark.webp differ diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..de2891e --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,39 @@ +{ + "name": "frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "eslint ." + }, + "devDependencies": { + "@eslint/js": "^9.39.2", + "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@typescript-eslint/eslint-plugin": "^8.50.1", + "@typescript-eslint/parser": "^8.50.1", + "autoprefixer": "^10.4.23", + "eslint": "^9.39.2", + "eslint-plugin-svelte": "^3.13.1", + "lucide-svelte": "^0.562.0", + "postcss": "^8.5.6", + "prettier": "^3.7.4", + "prettier-plugin-svelte": "^3.4.1", + "svelte": "^5.45.6", + "svelte-check": "^4.3.4", + "svelte-eslint-parser": "^1.4.1", + "svelte-i18n": "^4.0.1", + "tailwindcss": "^3.4.19", + "typescript": "^5.9.3", + "vite": "^7.2.6" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..769d97a --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,3008 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@eslint/js': + specifier: ^9.39.2 + version: 9.39.2 + '@sveltejs/adapter-auto': + specifier: ^7.0.0 + version: 7.0.0(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)))(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7))) + '@sveltejs/adapter-static': + specifier: ^3.0.10 + version: 3.0.10(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)))(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7))) + '@sveltejs/kit': + specifier: ^2.49.1 + version: 2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)))(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)) + '@sveltejs/vite-plugin-svelte': + specifier: ^6.2.1 + version: 6.2.1(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)) + '@typescript-eslint/eslint-plugin': + specifier: ^8.50.1 + version: 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.50.1 + version: 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + autoprefixer: + specifier: ^10.4.23 + version: 10.4.23(postcss@8.5.6) + eslint: + specifier: ^9.39.2 + version: 9.39.2(jiti@1.21.7) + eslint-plugin-svelte: + specifier: ^3.13.1 + version: 3.13.1(eslint@9.39.2(jiti@1.21.7))(svelte@5.46.1) + lucide-svelte: + specifier: ^0.562.0 + version: 0.562.0(svelte@5.46.1) + postcss: + specifier: ^8.5.6 + version: 8.5.6 + prettier: + specifier: ^3.7.4 + version: 3.7.4 + prettier-plugin-svelte: + specifier: ^3.4.1 + version: 3.4.1(prettier@3.7.4)(svelte@5.46.1) + svelte: + specifier: ^5.45.6 + version: 5.46.1 + svelte-check: + specifier: ^4.3.4 + version: 4.3.5(picomatch@4.0.3)(svelte@5.46.1)(typescript@5.9.3) + svelte-eslint-parser: + specifier: ^1.4.1 + version: 1.4.1(svelte@5.46.1) + svelte-i18n: + specifier: ^4.0.1 + version: 4.0.1(svelte@5.46.1) + tailwindcss: + specifier: ^3.4.19 + version: 3.4.19 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^7.2.6 + version: 7.3.0(jiti@1.21.7) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@esbuild/aix-ppc64@0.19.12': + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.19.12': + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.19.12': + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.19.12': + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.19.12': + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.19.12': + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.19.12': + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.19.12': + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.19.12': + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.19.12': + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.19.12': + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.19.12': + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.19.12': + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.19.12': + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.19.12': + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.19.12': + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.19.12': + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.19.12': + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.19.12': + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.19.12': + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.19.12': + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.19.12': + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.19.12': + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + 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} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@formatjs/ecma402-abstract@2.3.6': + resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} + + '@formatjs/fast-memoize@2.2.7': + resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + + '@formatjs/icu-messageformat-parser@2.11.4': + resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} + + '@formatjs/icu-skeleton-parser@1.8.16': + resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} + + '@formatjs/intl-localematcher@0.6.2': + resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rollup/rollup-android-arm-eabi@4.54.0': + resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.54.0': + resolution: {integrity: sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.54.0': + resolution: {integrity: sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.54.0': + resolution: {integrity: sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.54.0': + resolution: {integrity: sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.54.0': + resolution: {integrity: sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': + resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.54.0': + resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.54.0': + resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.54.0': + resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.54.0': + resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.54.0': + resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.54.0': + resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.54.0': + resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.54.0': + resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.54.0': + resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.54.0': + resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.54.0': + resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.54.0': + resolution: {integrity: sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.54.0': + resolution: {integrity: sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.54.0': + resolution: {integrity: sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.54.0': + resolution: {integrity: sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==} + cpu: [x64] + os: [win32] + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@sveltejs/acorn-typescript@1.0.8': + resolution: {integrity: sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/adapter-auto@7.0.0': + resolution: {integrity: sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw==} + peerDependencies: + '@sveltejs/kit': ^2.0.0 + + '@sveltejs/adapter-static@3.0.10': + resolution: {integrity: sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==} + peerDependencies: + '@sveltejs/kit': ^2.0.0 + + '@sveltejs/kit@2.49.2': + resolution: {integrity: sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + + '@sveltejs/vite-plugin-svelte-inspector@5.0.1': + resolution: {integrity: sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@sveltejs/vite-plugin-svelte@6.2.1': + resolution: {integrity: sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@typescript-eslint/eslint-plugin@8.50.1': + resolution: {integrity: sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.50.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.50.1': + resolution: {integrity: sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.50.1': + resolution: {integrity: sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.50.1': + resolution: {integrity: sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.50.1': + resolution: {integrity: sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.50.1': + resolution: {integrity: sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.50.1': + resolution: {integrity: sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.50.1': + resolution: {integrity: sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.50.1': + resolution: {integrity: sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.50.1': + resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + autoprefixer@10.4.23: + resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.9.11: + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001761: + resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cli-color@2.0.4: + resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==} + engines: {node: '>=0.10'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + devalue@5.6.1: + resolution: {integrity: sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + + es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + + es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + + es6-weak-map@2.0.3: + resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==} + + esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-svelte@3.13.1: + resolution: {integrity: sha512-Ng+kV/qGS8P/isbNYVE3sJORtubB+yLEcYICMkUWNaDTb0SwZni/JhAYXh/Dz/q2eThUwWY0VMPZ//KYD1n3eQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.1 || ^9.0.0 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrap@2.2.1: + resolution: {integrity: sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + + ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + globalyzer@0.1.0: + resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + intl-messageformat@10.7.18: + resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lru-queue@0.1.0: + resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} + + lucide-svelte@0.562.0: + resolution: {integrity: sha512-kSJDH/55lf0mun/o4nqWBXOcq0fWYzPeIjbTD97ywoeumAB9kWxtM06gC7oynqjtK3XhAljWSz5RafIzPEYIQA==} + peerDependencies: + svelte: ^3 || ^4 || ^5.0.0-next.42 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + memoizee@0.4.17: + resolution: {integrity: sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==} + engines: {node: '>=0.12'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@3.1.4: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-plugin-svelte@3.4.1: + resolution: {integrity: sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg==} + peerDependencies: + prettier: ^3.0.0 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + engines: {node: '>=14'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.54.0: + resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svelte-check@4.3.5: + resolution: {integrity: sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte-eslint-parser@1.4.1: + resolution: {integrity: sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.24.0} + peerDependencies: + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + svelte-i18n@4.0.1: + resolution: {integrity: sha512-jaykGlGT5PUaaq04JWbJREvivlCnALtT+m87Kbm0fxyYHynkQaxQMnIKHLm2WeIuBRoljzwgyvz0Z6/CMwfdmQ==} + engines: {node: '>= 16'} + hasBin: true + peerDependencies: + svelte: ^3 || ^4 || ^5 + + svelte@5.46.1: + resolution: {integrity: sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA==} + engines: {node: '>=18'} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + timers-ext@0.1.8: + resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} + engines: {node: '>=0.12'} + + tiny-glob@0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + ts-api-utils@2.2.0: + resolution: {integrity: sha512-L6f5oQRAoLU1RwXz0Ab9mxsE7LtxeVB6AIR1lpkZMsOyg/JXeaxBaXa/FVCBZyNr9S9I4wkHrlZTklX+im+WMw==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@7.3.0: + resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.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' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.1: + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@esbuild/aix-ppc64@0.19.12': + optional: true + + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.19.12': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.19.12': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.19.12': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.19.12': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.19.12': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.19.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.19.12': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.19.12': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.19.12': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.19.12': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.19.12': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.19.12': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.19.12': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.19.12': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.19.12': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.19.12': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.19.12': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.19.12': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.19.12': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.19.12': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.19.12': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.19.12': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@1.21.7))': + dependencies: + eslint: 9.39.2(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.2': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@formatjs/ecma402-abstract@2.3.6': + dependencies: + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/intl-localematcher': 0.6.2 + decimal.js: 10.6.0 + tslib: 2.8.1 + + '@formatjs/fast-memoize@2.2.7': + dependencies: + tslib: 2.8.1 + + '@formatjs/icu-messageformat-parser@2.11.4': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/icu-skeleton-parser': 1.8.16 + tslib: 2.8.1 + + '@formatjs/icu-skeleton-parser@1.8.16': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.6.2': + dependencies: + tslib: 2.8.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@polka/url@1.0.0-next.29': {} + + '@rollup/rollup-android-arm-eabi@4.54.0': + optional: true + + '@rollup/rollup-android-arm64@4.54.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.54.0': + optional: true + + '@rollup/rollup-darwin-x64@4.54.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.54.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.54.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.54.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.54.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.54.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.54.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.54.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.54.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.54.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.54.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.54.0': + optional: true + + '@standard-schema/spec@1.1.0': {} + + '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)': + dependencies: + acorn: 8.15.0 + + '@sveltejs/adapter-auto@7.0.0(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)))(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)))': + dependencies: + '@sveltejs/kit': 2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)))(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)) + + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)))(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)))': + dependencies: + '@sveltejs/kit': 2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)))(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)) + + '@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)))(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7))': + dependencies: + '@standard-schema/spec': 1.1.0 + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)) + '@types/cookie': 0.6.0 + acorn: 8.15.0 + cookie: 0.6.0 + devalue: 5.6.1 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + sade: 1.8.1 + set-cookie-parser: 2.7.2 + sirv: 3.0.2 + svelte: 5.46.1 + vite: 7.3.0(jiti@1.21.7) + + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)))(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)) + debug: 4.4.3 + svelte: 5.46.1 + vite: 7.3.0(jiti@1.21.7) + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)))(svelte@5.46.1)(vite@7.3.0(jiti@1.21.7)) + debug: 4.4.3 + deepmerge: 4.3.1 + magic-string: 0.30.21 + svelte: 5.46.1 + vite: 7.3.0(jiti@1.21.7) + vitefu: 1.1.1(vite@7.3.0(jiti@1.21.7)) + transitivePeerDependencies: + - supports-color + + '@types/cookie@0.6.0': {} + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@typescript-eslint/eslint-plugin@8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/type-utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.1 + eslint: 9.39.2(jiti@1.21.7) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.2.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.1 + debug: 4.4.3 + eslint: 9.39.2(jiti@1.21.7) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.50.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.50.1': + dependencies: + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/visitor-keys': 8.50.1 + + '@typescript-eslint/tsconfig-utils@8.50.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.2(jiti@1.21.7) + ts-api-utils: 2.2.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.50.1': {} + + '@typescript-eslint/typescript-estree@8.50.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.50.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/visitor-keys': 8.50.1 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.2.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.50.1': + dependencies: + '@typescript-eslint/types': 8.50.1 + eslint-visitor-keys: 4.2.1 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + autoprefixer@10.4.23(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001761 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.9.11: {} + + binary-extensions@2.3.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001761 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + callsites@3.1.0: {} + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001761: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cli-color@2.0.4: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-iterator: 2.0.3 + memoizee: 0.4.17 + timers-ext: 0.1.8 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@4.1.1: {} + + concat-map@0.0.1: {} + + cookie@0.6.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + d@1.0.2: + dependencies: + es5-ext: 0.10.64 + type: 2.7.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + devalue@5.6.1: {} + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + electron-to-chromium@1.5.267: {} + + es5-ext@0.10.64: + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + + es6-iterator@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + + es6-symbol@3.1.4: + dependencies: + d: 1.0.2 + ext: 1.7.0 + + es6-weak-map@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + + esbuild@0.19.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-svelte@3.13.1(eslint@9.39.2(jiti@1.21.7))(svelte@5.46.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) + '@jridgewell/sourcemap-codec': 1.5.5 + eslint: 9.39.2(jiti@1.21.7) + esutils: 2.0.3 + globals: 16.5.0 + known-css-properties: 0.37.0 + postcss: 8.5.6 + postcss-load-config: 3.1.4(postcss@8.5.6) + postcss-safe-parser: 7.0.1(postcss@8.5.6) + semver: 7.7.3 + svelte-eslint-parser: 1.4.1(svelte@5.46.1) + optionalDependencies: + svelte: 5.46.1 + transitivePeerDependencies: + - ts-node + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.2(jiti@1.21.7): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 1.21.7 + transitivePeerDependencies: + - supports-color + + esm-env@1.2.2: {} + + esniff@2.0.1: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.3 + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrap@2.2.1: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + event-emitter@0.3.5: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + + ext@1.7.0: + dependencies: + type: 2.7.3 + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + fraction.js@5.3.4: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.5.0: {} + + globalyzer@0.1.0: {} + + globrex@0.1.2: {} + + has-flag@4.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + intl-messageformat@10.7.18: + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/icu-messageformat-parser': 2.11.4 + tslib: 2.8.1 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-promise@2.2.2: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + isexe@2.0.0: {} + + jiti@1.21.7: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@4.1.5: {} + + known-css-properties@0.37.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@2.1.0: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + locate-character@3.0.0: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lru-queue@0.1.0: + dependencies: + es5-ext: 0.10.64 + + lucide-svelte@0.562.0(svelte@5.46.1): + dependencies: + svelte: 5.46.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + memoizee@0.4.17: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-weak-map: 2.0.3 + event-emitter: 0.3.5 + is-promise: 2.2.2 + lru-queue: 0.1.0 + next-tick: 1.1.0 + timers-ext: 0.1.8 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + mri@1.2.0: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + next-tick@1.1.0: {} + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + postcss-import@15.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-load-config@3.1.4(postcss@8.5.6): + dependencies: + lilconfig: 2.1.0 + yaml: 1.10.2 + optionalDependencies: + postcss: 8.5.6 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.6 + + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-safe-parser@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-scss@4.0.9(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-plugin-svelte@3.4.1(prettier@3.7.4)(svelte@5.46.1): + dependencies: + prettier: 3.7.4 + svelte: 5.46.1 + + prettier@3.7.4: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: {} + + resolve-from@4.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rollup@4.54.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.54.0 + '@rollup/rollup-android-arm64': 4.54.0 + '@rollup/rollup-darwin-arm64': 4.54.0 + '@rollup/rollup-darwin-x64': 4.54.0 + '@rollup/rollup-freebsd-arm64': 4.54.0 + '@rollup/rollup-freebsd-x64': 4.54.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.54.0 + '@rollup/rollup-linux-arm-musleabihf': 4.54.0 + '@rollup/rollup-linux-arm64-gnu': 4.54.0 + '@rollup/rollup-linux-arm64-musl': 4.54.0 + '@rollup/rollup-linux-loong64-gnu': 4.54.0 + '@rollup/rollup-linux-ppc64-gnu': 4.54.0 + '@rollup/rollup-linux-riscv64-gnu': 4.54.0 + '@rollup/rollup-linux-riscv64-musl': 4.54.0 + '@rollup/rollup-linux-s390x-gnu': 4.54.0 + '@rollup/rollup-linux-x64-gnu': 4.54.0 + '@rollup/rollup-linux-x64-musl': 4.54.0 + '@rollup/rollup-openharmony-arm64': 4.54.0 + '@rollup/rollup-win32-arm64-msvc': 4.54.0 + '@rollup/rollup-win32-ia32-msvc': 4.54.0 + '@rollup/rollup-win32-x64-gnu': 4.54.0 + '@rollup/rollup-win32-x64-msvc': 4.54.0 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + semver@7.7.3: {} + + set-cookie-parser@2.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + strip-json-comments@3.1.1: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svelte-check@4.3.5(picomatch@4.0.3)(svelte@5.46.1)(typescript@5.9.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.3) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.46.1 + typescript: 5.9.3 + transitivePeerDependencies: + - picomatch + + svelte-eslint-parser@1.4.1(svelte@5.46.1): + dependencies: + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + postcss: 8.5.6 + postcss-scss: 4.0.9(postcss@8.5.6) + postcss-selector-parser: 7.1.1 + optionalDependencies: + svelte: 5.46.1 + + svelte-i18n@4.0.1(svelte@5.46.1): + dependencies: + cli-color: 2.0.4 + deepmerge: 4.3.1 + esbuild: 0.19.12 + estree-walker: 2.0.2 + intl-messageformat: 10.7.18 + sade: 1.8.1 + svelte: 5.46.1 + tiny-glob: 0.2.9 + + svelte@5.46.1: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) + '@types/estree': 1.0.8 + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.6.1 + esm-env: 1.2.2 + esrap: 2.2.1 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + timers-ext@0.1.8: + dependencies: + es5-ext: 0.10.64 + next-tick: 1.1.0 + + tiny-glob@0.2.9: + dependencies: + globalyzer: 0.1.0 + globrex: 0.1.2 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + ts-api-utils@2.2.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-interface-checker@0.1.13: {} + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type@2.7.3: {} + + typescript@5.9.3: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vite@7.3.0(jiti@1.21.7): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.54.0 + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 + jiti: 1.21.7 + + vitefu@1.1.1(vite@7.3.0(jiti@1.21.7)): + optionalDependencies: + vite: 7.3.0(jiti@1.21.7) + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yaml@1.10.2: {} + + yocto-queue@0.1.0: {} + + zimmerframe@1.1.4: {} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..7b75c83 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 0000000..6d5c694 --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1,83 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } + + /* Custom thin scrollbar */ + ::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + ::-webkit-scrollbar-track { + @apply bg-transparent; + } + + ::-webkit-scrollbar-thumb { + @apply bg-muted-foreground/20 rounded-full transition-colors; + } + + ::-webkit-scrollbar-thumb:hover { + @apply bg-muted-foreground/40; + } + + /* Firefox */ + * { + scrollbar-width: thin; + scrollbar-color: hsl(var(--muted-foreground) / 0.2) transparent; + } +} diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/frontend/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/frontend/src/app.html b/frontend/src/app.html new file mode 100644 index 0000000..f273cc5 --- /dev/null +++ b/frontend/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/frontend/src/lib/assets/favicon.svg b/frontend/src/lib/assets/favicon.svg new file mode 100644 index 0000000..cc5dc66 --- /dev/null +++ b/frontend/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/frontend/src/lib/components/SearchBar.svelte b/frontend/src/lib/components/SearchBar.svelte new file mode 100644 index 0000000..9252df2 --- /dev/null +++ b/frontend/src/lib/components/SearchBar.svelte @@ -0,0 +1,22 @@ + + +
+
+

{$t('common.title')}

+

{$t('common.subtitle')}

+
+
+ + +
+
diff --git a/frontend/src/lib/components/SoftwareCard.svelte b/frontend/src/lib/components/SoftwareCard.svelte new file mode 100644 index 0000000..01c3e47 --- /dev/null +++ b/frontend/src/lib/components/SoftwareCard.svelte @@ -0,0 +1,369 @@ + + +
+
+
+
+ {#if software.avatar_url} + {software.name} + {:else} + + {/if} +
+
+ + + + {#if !software.is_private && software.gitea_url} + + Gitea + + {/if} +
+
+
+

+ {software.name.replace(/-/g, ' ')} +

+ {#if software.license} + + {software.license} + + {/if} +
+

+ {software.description || $t('common.repoFor', { values: { name: software.name } })} +

+ {#if software.topics && software.topics.length > 0} +
+ {#each software.topics as topic} + + {topic} + + {/each} +
+ {/if} +
+ +
+ {#if software.releases && software.releases.length > 0} + {@const latest = software.releases[0]} +
+
+
+
+ {$t('common.latest')}: {latest.tag_name} +
+ {#if latest.body} + + {/if} + {#if availableOSs.length > 0} +
+ {#each availableOSs as os} + {@const brand = getOSIcon(os)} + + {/each} +
+ {/if} +
+ {#if software.releases.length > 1} + + {/if} +
+ + {#if showReleaseNotes && latest.body} +
+ {latest.body} +
+ {/if} + + {#if expandedReleases[software.name]} +
+ {#each software.releases as release, i} + {@const filteredAssets = release.assets.filter( + (a) => selectedOSs.length === 0 || selectedOSs.includes(a.os.toLowerCase()) + )} + {#if filteredAssets.length > 0} +
+
+ {release.tag_name} {i === 0 ? `(${$t('common.latest')})` : ''} +
+
+ {#each filteredAssets as asset} + {@const brand = getOSIcon(asset.os)} + {@const FallbackIcon = getFallbackIcon(asset.os)} + + {/each} +
+
+ {/if} + {/each} +
+ {:else} + {@const filteredLatestAssets = latest.assets.filter( + (a) => selectedOSs.length === 0 || selectedOSs.includes(a.os.toLowerCase()) + )} +
+ {#if filteredLatestAssets.length > 0} + {#each filteredLatestAssets as asset} + {@const brand = getOSIcon(asset.os)} + {@const FallbackIcon = getFallbackIcon(asset.os)} + + {/each} + {:else} +
+ {$t('common.noMatchingAssets')} +
+ {/if} +
+ {/if} +
+ {:else} +
+ {$t('common.noReleases')} +
+ {/if} +
+
diff --git a/frontend/src/lib/components/icons/BrandIcon.svelte b/frontend/src/lib/components/icons/BrandIcon.svelte new file mode 100644 index 0000000..ee2c048 --- /dev/null +++ b/frontend/src/lib/components/icons/BrandIcon.svelte @@ -0,0 +1,35 @@ + + + diff --git a/frontend/src/lib/i18n/index.ts b/frontend/src/lib/i18n/index.ts new file mode 100644 index 0000000..be093b2 --- /dev/null +++ b/frontend/src/lib/i18n/index.ts @@ -0,0 +1,13 @@ +import { init, register, getLocaleFromNavigator } from 'svelte-i18n'; + +const defaultLocale = 'en'; + +register('en', () => import('./locales/en.json')); +register('de', () => import('./locales/de.json')); +register('ru', () => import('./locales/ru.json')); +register('it', () => import('./locales/it.json')); + +init({ + fallbackLocale: defaultLocale, + initialLocale: getLocaleFromNavigator() || defaultLocale, +}); diff --git a/frontend/src/lib/i18n/locales/de.json b/frontend/src/lib/i18n/locales/de.json new file mode 100644 index 0000000..8af772b --- /dev/null +++ b/frontend/src/lib/i18n/locales/de.json @@ -0,0 +1,45 @@ +{ + "common": { + "title": "Softwareverteilung", + "subtitle": "Sicherer Download von Build-Binärdateien und Containern.", + "search": "Software suchen...", + "noSoftware": "Keine Software gefunden", + "tryAdjusting": "Versuchen Sie, Ihre Suchanfrage anzupassen.", + "latest": "Aktuellste", + "releaseNotes": "Versionshinweise", + "notes": "Notizen", + "openSource": "Open-Source", + "mit": "MIT", + "previousReleases": "Vorherige Versionen", + "hide": "Ausblenden", + "noReleases": "Keine Versionen verfügbar", + "viewOnGitea": "Auf Gitea ansehen", + "checksumVerified": "SHA256 Verifiziert", + "checksumMissing": "Prüfsumme nicht verfügbar", + "rightsReserved": "Alle Rechte vorbehalten.", + "repoFor": "Software-Repository für {name}.", + "privacy": "Datenschutzerklärung", + "terms": "Nutzungsbedingungen", + "disclaimer": "Haftungsausschluss", + "downloadDoc": "Herunterladen (.txt)", + "license": "Lizenz", + "sbom": "SBOM", + "backToSoftware": "Zurück zur Software", + "docLoadError": "Dokument konnte nicht geladen werden", + "docNotFound": "Das angeforderte rechtliche Dokument konnte nicht gefunden werden.", + "errorTitle": "Software konnte nicht geladen werden", + "errorMessage": "Der Server ist möglicherweise nicht erreichbar. Bitte versuchen Sie es später erneut.", + "retry": "Wiederholen", + "noMatchingAssets": "Keine Dateien entsprechen den Filtern" + }, + "os": { + "windows": "Windows", + "linux": "Linux", + "macos": "macOS", + "android": "Android", + "freebsd": "FreeBSD", + "openbsd": "OpenBSD", + "arm": "ARM/AArch64", + "unknown": "Andere" + } +} diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json new file mode 100644 index 0000000..b70073d --- /dev/null +++ b/frontend/src/lib/i18n/locales/en.json @@ -0,0 +1,45 @@ +{ + "common": { + "title": "Software Distribution", + "subtitle": "Securely download built binaries and containers.", + "search": "Search software...", + "noSoftware": "No software found", + "tryAdjusting": "Try adjusting your search query.", + "latest": "Latest", + "releaseNotes": "Release Notes", + "notes": "Notes", + "openSource": "Open-Source", + "mit": "MIT", + "previousReleases": "Previous Releases", + "hide": "Hide", + "noReleases": "No releases available", + "viewOnGitea": "View on Gitea", + "checksumVerified": "SHA256 Verified", + "checksumMissing": "Checksum not provided", + "rightsReserved": "All rights reserved.", + "repoFor": "Software repository for {name}.", + "privacy": "Privacy Policy", + "terms": "Terms of Service", + "disclaimer": "Legal Disclaimer", + "downloadDoc": "Download (.txt)", + "license": "License", + "sbom": "SBOM", + "backToSoftware": "Back to Software", + "docLoadError": "Failed to load document", + "docNotFound": "The requested legal document could not be found.", + "errorTitle": "Failed to load software", + "errorMessage": "The server might be down or unreachable. Please try again later.", + "retry": "Retry", + "noMatchingAssets": "No assets match selected filters" + }, + "os": { + "windows": "Windows", + "linux": "Linux", + "macos": "macOS", + "android": "Android", + "freebsd": "FreeBSD", + "openbsd": "OpenBSD", + "arm": "ARM/AArch64", + "unknown": "Other" + } +} diff --git a/frontend/src/lib/i18n/locales/it.json b/frontend/src/lib/i18n/locales/it.json new file mode 100644 index 0000000..13ea2a7 --- /dev/null +++ b/frontend/src/lib/i18n/locales/it.json @@ -0,0 +1,45 @@ +{ + "common": { + "title": "Distribuzione Software", + "subtitle": "Scarica in sicurezza binari e container compilati.", + "search": "Cerca software...", + "noSoftware": "Nessun software trovato", + "tryAdjusting": "Prova a modificare la query di ricerca.", + "latest": "Ultima", + "releaseNotes": "Note di rilascio", + "notes": "Note", + "openSource": "Open-Source", + "mit": "MIT", + "previousReleases": "Versioni precedenti", + "hide": "Nascondi", + "noReleases": "Nessun rilascio disponibile", + "viewOnGitea": "Visualizza su Gitea", + "checksumVerified": "SHA256 verificato", + "checksumMissing": "Checksum non fornito", + "rightsReserved": "Tutti i diritti riservati.", + "repoFor": "Repository software per {name}.", + "privacy": "Informativa sulla Privacy", + "terms": "Termini di Servizio", + "disclaimer": "Disclaimer Legale", + "downloadDoc": "Scarica (.txt)", + "license": "Licenza", + "sbom": "SBOM", + "backToSoftware": "Torna al Software", + "docLoadError": "Caricamento documento fallito", + "docNotFound": "Il documento legale richiesto non è stato trovato.", + "errorTitle": "Caricamento software fallito", + "errorMessage": "Il server potrebbe essere inattivo o irraggiungibile. Riprova più tardi.", + "retry": "Riprova", + "noMatchingAssets": "Nessun file corrisponde ai filtri selezionati" + }, + "os": { + "windows": "Windows", + "linux": "Linux", + "macos": "macOS", + "android": "Android", + "freebsd": "FreeBSD", + "openbsd": "OpenBSD", + "arm": "ARM/AArch64", + "unknown": "Altro" + } +} diff --git a/frontend/src/lib/i18n/locales/ru.json b/frontend/src/lib/i18n/locales/ru.json new file mode 100644 index 0000000..c4e062d --- /dev/null +++ b/frontend/src/lib/i18n/locales/ru.json @@ -0,0 +1,45 @@ +{ + "common": { + "title": "Распространение ПО", + "subtitle": "Безопасная загрузка готовых бинарных файлов и контейнеров.", + "search": "Поиск ПО...", + "noSoftware": "Программное обеспечение не найдено", + "tryAdjusting": "Попробуйте изменить поисковый запрос.", + "latest": "Последняя", + "releaseNotes": "Примечания к выпуску", + "notes": "Заметки", + "openSource": "Open-Source", + "mit": "MIT", + "previousReleases": "Предыдущие версии", + "hide": "Скрыть", + "noReleases": "Версии недоступны", + "viewOnGitea": "Посмотреть на Gitea", + "checksumVerified": "SHA256 подтвержден", + "checksumMissing": "Контрольная сумма не предоставлена", + "rightsReserved": "Все права защищены.", + "repoFor": "Репозиторий ПО для {name}.", + "privacy": "Политика конфиденциальности", + "terms": "Условия использования", + "disclaimer": "Отказ от ответственности", + "downloadDoc": "Скачать (.txt)", + "license": "Лицензия", + "sbom": "SBOM", + "backToSoftware": "Назад к ПО", + "docLoadError": "Не удалось загрузить документ", + "docNotFound": "Запрошенный юридический документ не найден.", + "errorTitle": "Не удалось загрузить ПО", + "errorMessage": "Сервер может быть недоступен. Пожалуйста, попробуйте позже.", + "retry": "Повторить", + "noMatchingAssets": "Нет файлов, соответствующих фильтрам" + }, + "os": { + "windows": "Windows", + "linux": "Linux", + "macos": "macOS", + "android": "Android", + "freebsd": "FreeBSD", + "openbsd": "OpenBSD", + "arm": "ARM/AArch64", + "unknown": "Другое" + } +} diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/frontend/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts new file mode 100644 index 0000000..54afe22 --- /dev/null +++ b/frontend/src/lib/types.ts @@ -0,0 +1,26 @@ +export interface Asset { + name: string; + size: number; + url: string; + os: string; + sha256?: string; + is_sbom: boolean; +} + +export interface Release { + tag_name: string; + body?: string; + assets: Asset[]; +} + +export interface Software { + name: string; + owner: string; + description: string; + releases: Release[]; + gitea_url: string; + topics: string[]; + license?: string; + is_private: boolean; + avatar_url?: string; +} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..45da5cd --- /dev/null +++ b/frontend/src/routes/+layout.svelte @@ -0,0 +1,162 @@ + + +{#if i18nReady} +
+ + +
+ {@render children()} +
+ + +
+{:else} +
+
+
+{/if} diff --git a/frontend/src/routes/+layout.ts b/frontend/src/routes/+layout.ts new file mode 100644 index 0000000..37ce563 --- /dev/null +++ b/frontend/src/routes/+layout.ts @@ -0,0 +1,3 @@ +export const ssr = false; +export const prerender = false; +export const trailingSlash = 'always'; diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte new file mode 100644 index 0000000..06c5677 --- /dev/null +++ b/frontend/src/routes/+page.svelte @@ -0,0 +1,106 @@ + + +
+ + + {#if loading} +
+ {#each Array(6) as _} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/each} +
+ {:else if error} +
+ +

{$t('common.errorTitle')}

+

{$t('common.errorMessage')}

+ +
+ {:else if filteredSoftware.length === 0} +
+ +

{$t('common.noSoftware')}

+

{$t('common.tryAdjusting')}

+
+ {:else} +
+ {#each filteredSoftware as software} + + {/each} +
+ {/if} +
diff --git a/frontend/src/routes/legal/[doc]/+page.svelte b/frontend/src/routes/legal/[doc]/+page.svelte new file mode 100644 index 0000000..3225079 --- /dev/null +++ b/frontend/src/routes/legal/[doc]/+page.svelte @@ -0,0 +1,81 @@ + + +
+
+ + + {$t('common.backToSoftware')} + + {#if !loading && !error} + + + {$t('common.downloadDoc')} + + {/if} +
+ +
+
+
+
+ +
+

+ {#if docType === 'privacy'} + {$t('common.privacy')} + {:else} + {$t(`common.${docType}`)} + {/if} +

+
+
+ +
+ {#if loading} +
+
+
+
+
+
+ {:else if error} +
+
{$t('common.docLoadError')}
+

{$t('common.docNotFound')}

+
+ {:else} +
{docContent}
+ {/if} +
+
+
diff --git a/frontend/static/.well-known/security.txt b/frontend/static/.well-known/security.txt new file mode 100644 index 0000000..87cee6f --- /dev/null +++ b/frontend/static/.well-known/security.txt @@ -0,0 +1,6 @@ +Contact: https://quad4.io +Expires: 2026-12-31T23:59:59.000Z +Preferred-Languages: en +Canonical: https://git.quad4.io/Quad4-Software/software-station/.well-known/security.txt +Policy: https://quad4.io/security + diff --git a/frontend/static/icons/gitea-dark.webp b/frontend/static/icons/gitea-dark.webp new file mode 100644 index 0000000..7b4ebb5 Binary files /dev/null and b/frontend/static/icons/gitea-dark.webp differ diff --git a/frontend/static/icons/gitea.webp b/frontend/static/icons/gitea.webp new file mode 100644 index 0000000..4e30d4e Binary files /dev/null and b/frontend/static/icons/gitea.webp differ diff --git a/frontend/static/logo.png b/frontend/static/logo.png new file mode 100644 index 0000000..1452133 Binary files /dev/null and b/frontend/static/logo.png differ diff --git a/frontend/static/robots.txt b/frontend/static/robots.txt new file mode 100644 index 0000000..2c5308f --- /dev/null +++ b/frontend/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: \ No newline at end of file diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js new file mode 100644 index 0000000..47346fa --- /dev/null +++ b/frontend/svelte.config.js @@ -0,0 +1,24 @@ +import adapter from '@sveltejs/adapter-static'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter({ + fallback: 'index.html', // for SPA + pages: 'build', + assets: 'build', + precompress: false, + strict: true, + }), + }, +}; + +export default config; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..1eabb4d --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,50 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{html,js,svelte,ts}'], + darkMode: 'class', + theme: { + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + }, + }, + plugins: [], +}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..2c2ed3c --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..b33227d --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,11 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()], + server: { + proxy: { + '/api': 'http://localhost:8080', + }, + }, +}); diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..78464dc --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module software-station + +go 1.25.4 + +require ( + github.com/go-chi/chi/v5 v5.2.3 + github.com/go-chi/httprate v0.15.0 +) + +require ( + github.com/go-chi/cors v1.2.2 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/unrolled/secure v1.17.0 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/time v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c439ed2 --- /dev/null +++ b/go.sum @@ -0,0 +1,20 @@ +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= +github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5g= +github.com/go-chi/httprate v0.15.0/go.mod h1:rzGHhVrsBn3IMLYDOZQsSU4fJNWcjui4fWKJcCId1R4= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= +github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= diff --git a/internal/api/constants.go b/internal/api/constants.go new file mode 100644 index 0000000..a3f09dc --- /dev/null +++ b/internal/api/constants.go @@ -0,0 +1,10 @@ +package api + +const ( + CompressionLevel = 5 + + LegalDir = "legal" + PrivacyFile = "privacy.txt" + TermsFile = "terms.txt" + DisclaimerFile = "disclaimer.txt" +) diff --git a/internal/api/handlers.go b/internal/api/handlers.go new file mode 100644 index 0000000..754830e --- /dev/null +++ b/internal/api/handlers.go @@ -0,0 +1,425 @@ +package api + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "sort" + "strings" + "sync" + "sync/atomic" + "time" + + "software-station/internal/models" + "software-station/internal/security" + "software-station/internal/stats" + + "golang.org/x/time/rate" +) + +type SoftwareCache struct { + mu sync.RWMutex + data []models.Software +} + +func (c *SoftwareCache) Get() []models.Software { + c.mu.RLock() + defer c.mu.RUnlock() + return c.data +} + +func (c *SoftwareCache) Set(data []models.Software) { + c.mu.Lock() + defer c.mu.Unlock() + c.data = data +} + +func (c *SoftwareCache) GetLock() *sync.RWMutex { + return &c.mu +} + +func (c *SoftwareCache) GetDataPtr() *[]models.Software { + return &c.data +} + +type Server struct { + GiteaToken string + SoftwareList *SoftwareCache + Stats *stats.Service + urlMap map[string]string + urlMapMu sync.RWMutex + rssCache atomic.Value // stores string + rssLastMod atomic.Value // stores time.Time + avatarCache string +} + +func NewServer(token string, initialSoftware []models.Software, statsService *stats.Service) *Server { + s := &Server{ + GiteaToken: token, + SoftwareList: &SoftwareCache{data: initialSoftware}, + Stats: statsService, + urlMap: make(map[string]string), + avatarCache: ".cache/avatars", + } + s.rssCache.Store("") + s.rssLastMod.Store(time.Time{}) + + if err := os.MkdirAll(s.avatarCache, 0750); err != nil { + log.Printf("Warning: failed to create avatar cache directory: %v", err) + } + + return s +} + +func (s *Server) RegisterURL(targetURL string) string { + hash := fmt.Sprintf("%x", sha256.Sum256([]byte(targetURL))) + s.urlMapMu.Lock() + s.urlMap[hash] = targetURL + s.urlMapMu.Unlock() + return hash +} + +func (s *Server) APISoftwareHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + softwareList := s.SoftwareList.Get() + + host := r.Host + scheme := "http" + if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" { + scheme = "https" + } + + proxiedList := make([]models.Software, len(softwareList)) + for i, sw := range softwareList { + proxiedList[i] = sw + // If private, hide Gitea URL completely. If public, we can show it for the repo link. + if sw.IsPrivate { + proxiedList[i].GiteaURL = "" + } + + // Proxy avatar if it exists + if sw.AvatarURL != "" { + hash := s.RegisterURL(sw.AvatarURL) + proxiedList[i].AvatarURL = fmt.Sprintf("%s://%s/api/avatar?id=%s", scheme, host, hash) + } + + proxiedList[i].Releases = make([]models.Release, len(sw.Releases)) + for j, rel := range sw.Releases { + proxiedList[i].Releases[j] = rel + proxiedList[i].Releases[j].Assets = make([]models.Asset, len(rel.Assets)) + for k, asset := range rel.Assets { + proxiedList[i].Releases[j].Assets[k] = asset + hash := s.RegisterURL(asset.URL) + proxiedList[i].Releases[j].Assets[k].URL = fmt.Sprintf("%s://%s/api/download?id=%s", scheme, host, hash) + } + } + } + + if err := json.NewEncoder(w).Encode(proxiedList); err != nil { + log.Printf("Error encoding software list: %v", err) + } +} + +func (s *Server) DownloadProxyHandler(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + if id == "" { + http.Error(w, "Missing id parameter", http.StatusBadRequest) + return + } + + s.urlMapMu.RLock() + targetURL, exists := s.urlMap[id] + s.urlMapMu.RUnlock() + + if !exists { + http.Error(w, "Invalid or expired download ID", http.StatusNotFound) + return + } + + fingerprint := security.GetRequestFingerprint(r, s.Stats) + ua := strings.ToLower(r.UserAgent()) + + isSpeedDownloader := false + for _, sd := range security.SpeedDownloaders { + if strings.Contains(ua, sd) { + isSpeedDownloader = true + break + } + } + + limit := security.DefaultDownloadLimit + burst := int(security.DefaultDownloadBurst) + if isSpeedDownloader { + limit = security.SpeedDownloaderLimit + burst = int(security.SpeedDownloaderBurst) + s.Stats.GlobalStats.Lock() + s.Stats.GlobalStats.LimitedRequests[fingerprint] = true + s.Stats.GlobalStats.Unlock() + } + + s.Stats.DownloadStats.Lock() + limiter, exists := s.Stats.DownloadStats.Limiters[fingerprint] + if !exists { + limiter = rate.NewLimiter(limit, burst) + s.Stats.DownloadStats.Limiters[fingerprint] = limiter + } + + s.Stats.KnownHashes.RLock() + data, exists := s.Stats.KnownHashes.Data[fingerprint] + s.Stats.KnownHashes.RUnlock() + + var totalDownloaded int64 + if exists { + totalDownloaded = atomic.LoadInt64(&data.TotalBytes) + } + + if totalDownloaded > security.HeavyDownloaderThreshold { + limiter.SetLimit(security.HeavyDownloaderLimit) + s.Stats.GlobalStats.Lock() + s.Stats.GlobalStats.LimitedRequests[fingerprint] = true + s.Stats.GlobalStats.Unlock() + } else { + limiter.SetLimit(limit) + } + s.Stats.DownloadStats.Unlock() + + req, err := http.NewRequest("GET", targetURL, nil) + if err != nil { + http.Error(w, "Failed to create request", http.StatusInternalServerError) + return + } + + // Forward Range headers for resumable downloads + if rangeHeader := r.Header.Get("Range"); rangeHeader != "" { + req.Header.Set("Range", rangeHeader) + } + if ifRangeHeader := r.Header.Get("If-Range"); ifRangeHeader != "" { + req.Header.Set("If-Range", ifRangeHeader) + } + + if s.GiteaToken != "" { + req.Header.Set("Authorization", "token "+s.GiteaToken) + } + + client := security.GetSafeHTTPClient(0) + resp, err := client.Do(req) + if err != nil { + http.Error(w, "Failed to fetch asset: "+err.Error(), http.StatusBadGateway) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent { + http.Error(w, "Gitea returned error: "+resp.Status, http.StatusBadGateway) + return + } + + // Copy all headers from the upstream response + for k, vv := range resp.Header { + for _, v := range vv { + w.Header().Add(k, v) + } + } + w.WriteHeader(resp.StatusCode) + + tr := &security.ThrottledReader{ + R: resp.Body, + Limiter: limiter, + Fingerprint: fingerprint, + Stats: s.Stats, + } + + n, err := io.Copy(w, tr) + if err != nil { + log.Printf("Error copying proxy response: %v", err) + } + if n > 0 { + s.Stats.GlobalStats.Lock() + s.Stats.GlobalStats.SuccessDownloads[fingerprint] = true + s.Stats.GlobalStats.Unlock() + } + + s.Stats.SaveHashes() +} + +func (s *Server) LegalHandler(w http.ResponseWriter, r *http.Request) { + doc := r.URL.Query().Get("doc") + var filename string + + switch doc { + case "privacy": + filename = PrivacyFile + case "terms": + filename = TermsFile + case "disclaimer": + filename = DisclaimerFile + default: + http.Error(w, "Invalid document", http.StatusBadRequest) + return + } + + path := filepath.Join(LegalDir, filename) + data, err := os.ReadFile(path) // #nosec G304 + if err != nil { + http.Error(w, "Document not found", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + // If download parameter is present, set Content-Disposition + if r.URL.Query().Get("download") == "true" { + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) + } + if _, err := w.Write(data); err != nil { + log.Printf("Error writing legal document: %v", err) + } +} + +func (s *Server) AvatarHandler(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + if id == "" { + http.Error(w, "Missing id parameter", http.StatusBadRequest) + return + } + + cachePath := filepath.Join(s.avatarCache, id) + if _, err := os.Stat(cachePath); err == nil { + // Serve from cache + w.Header().Set("Cache-Control", "public, max-age=86400") + http.ServeFile(w, r, cachePath) + return + } + + s.urlMapMu.RLock() + targetURL, exists := s.urlMap[id] + s.urlMapMu.RUnlock() + + if !exists { + http.Error(w, "Invalid or expired avatar ID", http.StatusNotFound) + return + } + + req, err := http.NewRequest("GET", targetURL, nil) + if err != nil { + http.Error(w, "Failed to create request", http.StatusInternalServerError) + return + } + + if s.GiteaToken != "" { + req.Header.Set("Authorization", "token "+s.GiteaToken) + } + + client := security.GetSafeHTTPClient(0) + resp, err := client.Do(req) + if err != nil { + http.Error(w, "Failed to fetch avatar: "+err.Error(), http.StatusBadGateway) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + http.Error(w, "Gitea returned error: "+resp.Status, http.StatusBadGateway) + return + } + + // Copy data to cache and response simultaneously + data, err := io.ReadAll(resp.Body) + if err != nil { + http.Error(w, "Failed to read avatar data", http.StatusInternalServerError) + return + } + + if err := os.WriteFile(cachePath, data, 0600); err != nil { + log.Printf("Warning: failed to cache avatar: %v", err) + } + + w.Header().Set("Content-Type", resp.Header.Get("Content-Type")) + w.Header().Set("Cache-Control", "public, max-age=86400") + _, _ = w.Write(data) +} + +func (s *Server) RSSHandler(w http.ResponseWriter, r *http.Request) { + softwareList := s.SoftwareList.Get() + targetSoftware := r.URL.Query().Get("software") + + host := r.Host + scheme := "http" + if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" { + scheme = "https" + } + baseURL := fmt.Sprintf("%s://%s", scheme, host) + + // Collect all releases and sort by date + type item struct { + Software models.Software + Release models.Release + } + var items []item + for _, sw := range softwareList { + if targetSoftware != "" && sw.Name != targetSoftware { + continue + } + for _, rel := range sw.Releases { + items = append(items, item{Software: sw, Release: rel}) + } + } + + sort.Slice(items, func(i, j int) bool { + return items[i].Release.CreatedAt.After(items[j].Release.CreatedAt) + }) + + feedTitle := "Software Updates - Software Station" + feedDescription := "Latest software releases and updates" + selfLink := baseURL + "/api/rss" + if targetSoftware != "" { + feedTitle = fmt.Sprintf("%s Updates - Software Station", targetSoftware) + feedDescription = fmt.Sprintf("Latest releases and updates for %s", targetSoftware) + selfLink = fmt.Sprintf("%s/api/rss?software=%s", baseURL, targetSoftware) + } + + var b strings.Builder + b.WriteString(` + + + ` + feedTitle + ` + ` + baseURL + ` + ` + feedDescription + ` + en-us + ` + time.Now().Format(time.RFC1123Z) + ` + +`) + + for i, it := range items { + if i >= 50 { + break + } + title := fmt.Sprintf("%s %s", it.Software.Name, it.Release.TagName) + link := baseURL + description := it.Software.Description + if it.Release.Body != "" { + description = it.Release.Body + } + + fmt.Fprintf(&b, ` + %s + %s + + %s-%s + %s + +`, title, link, description, it.Software.Name, it.Release.TagName, it.Release.CreatedAt.Format(time.RFC1123Z)) + } + + b.WriteString(` +`) + + w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8") + w.Header().Set("Cache-Control", "public, max-age=300") + _, _ = w.Write([]byte(b.String())) +} diff --git a/internal/cache/cache.go b/internal/cache/cache.go new file mode 100644 index 0000000..cd2c3d1 --- /dev/null +++ b/internal/cache/cache.go @@ -0,0 +1,44 @@ +package cache + +import ( + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "software-station/internal/models" +) + +const cacheDir = ".cache" + +func init() { + if err := os.MkdirAll(cacheDir, 0750); err != nil { + log.Printf("Warning: failed to create cache directory: %v", err) + } +} + +func GetCachePath(owner, repo string) string { + return filepath.Join(cacheDir, filepath.Clean(fmt.Sprintf("%s_%s.json", owner, repo))) +} + +func SaveToCache(owner, repo string, software models.Software) error { + path := GetCachePath(owner, repo) + data, err := json.MarshalIndent(software, "", " ") + if err != nil { + return err + } + return os.WriteFile(path, data, 0600) +} + +func GetFromCache(owner, repo string) (*models.Software, error) { + path := GetCachePath(owner, repo) + data, err := os.ReadFile(path) // #nosec G304 + if err != nil { + return nil, err + } + var software models.Software + if err := json.Unmarshal(data, &software); err != nil { + return nil, err + } + return &software, nil +} diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go new file mode 100644 index 0000000..53aade3 --- /dev/null +++ b/internal/cache/cache_test.go @@ -0,0 +1,43 @@ +package cache + +import ( + "os" + "software-station/internal/models" + "testing" +) + +func TestCache(t *testing.T) { + owner := "test-owner" + repo := "test-repo" + software := models.Software{ + Name: "test-repo", + Owner: "test-owner", + Description: "test desc", + } + + // Clean up before and after + path := GetCachePath(owner, repo) + os.Remove(path) + defer os.Remove(path) + + // Test GetFromCache missing + _, err := GetFromCache(owner, repo) + if err == nil { + t.Error("expected error for missing cache") + } + + // Test SaveToCache + err = SaveToCache(owner, repo, software) + if err != nil { + t.Fatalf("failed to save to cache: %v", err) + } + + // Test GetFromCache success + cached, err := GetFromCache(owner, repo) + if err != nil { + t.Fatalf("failed to get from cache: %v", err) + } + if cached.Name != software.Name || cached.Owner != software.Owner { + t.Errorf("cached data mismatch: %+v", cached) + } +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..f1f53cd --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,86 @@ +package config + +import ( + "bufio" + "io" + "log" + "net/http" + "os" + "strings" + + "software-station/internal/cache" + "software-station/internal/gitea" + "software-station/internal/models" +) + +func LoadSoftware(path, server, token string) []models.Software { + return LoadSoftwareExtended(path, server, token, true) +} + +func LoadSoftwareExtended(path, server, token string, useCache bool) []models.Software { + var reader io.Reader + + if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") { + resp, err := http.Get(path) // #nosec G107 + if err != nil { + log.Printf("Error fetching remote config: %v", err) + return nil + } + defer resp.Body.Close() + reader = resp.Body + } else { + file, err := os.Open(path) // #nosec G304 + if err != nil { + log.Printf("Warning: config file %s not found", path) + return nil + } + defer file.Close() + reader = file + } + + var softwareList []models.Software + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + parts := strings.Split(line, "/") + if len(parts) == 2 { + owner, repo := parts[0], parts[1] + + // Try to get from cache first + if useCache { + if cached, err := cache.GetFromCache(owner, repo); err == nil { + softwareList = append(softwareList, *cached) + continue + } + } + + desc, topics, license, isPrivate, avatarURL, err := gitea.FetchRepoInfo(server, token, owner, repo) + if err != nil { + log.Printf("Error fetching repo info for %s/%s: %v", owner, repo, err) + } + releases, err := gitea.FetchReleases(server, token, owner, repo) + if err != nil { + log.Printf("Error fetching releases for %s/%s: %v", owner, repo, err) + } + sw := models.Software{ + Name: repo, + Owner: owner, + Description: desc, + Releases: releases, + GiteaURL: server, + Topics: topics, + License: license, + IsPrivate: isPrivate, + AvatarURL: avatarURL, + } + softwareList = append(softwareList, sw) + if err := cache.SaveToCache(owner, repo, sw); err != nil { + log.Printf("Error saving to cache for %s/%s: %v", owner, repo, err) + } + } + } + return softwareList +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..751d6ba --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,62 @@ +package config + +import ( + "net/http" + "net/http/httptest" + "os" + "software-station/internal/models" + "sync" + "testing" + "time" +) + +func TestConfig(t *testing.T) { + // Test Local Config + configPath := "test_software.txt" + os.WriteFile(configPath, []byte("Owner/Repo\n#Comment\n\nOwner2/Repo2"), 0644) + defer os.Remove(configPath) + + // Mock Gitea + mockGitea := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"description": "Test", "topics": [], "license": {"name": "MIT"}}`)) + })) + defer mockGitea.Close() + + list := LoadSoftware(configPath, mockGitea.URL, "") + if len(list) != 2 { + t.Errorf("expected 2 repos, got %d", len(list)) + } + + // Test Remote Config + mockRemote := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Owner3/Repo3")) + })) + defer mockRemote.Close() + + listRemote := LoadSoftware(mockRemote.URL, mockGitea.URL, "") + if len(listRemote) != 1 || listRemote[0].Name != "Repo3" { + t.Errorf("expected Repo3, got %v", listRemote) + } +} + +func TestBackgroundUpdater(t *testing.T) { + configPath := "test_updater.txt" + os.WriteFile(configPath, []byte("Owner/Repo"), 0644) + defer os.Remove(configPath) + + mockGitea := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"description": "Updated", "topics": [], "license": {"name": "MIT"}}`)) + })) + defer mockGitea.Close() + + var mu sync.RWMutex + softwareList := &[]models.Software{} + StartBackgroundUpdater(configPath, mockGitea.URL, "", &mu, softwareList, 100*time.Millisecond) + + // Wait for ticker + time.Sleep(200 * time.Millisecond) + + if len(*softwareList) == 0 { + t.Error("softwareList was not updated by background updater") + } +} diff --git a/internal/config/constants.go b/internal/config/constants.go new file mode 100644 index 0000000..18cb204 --- /dev/null +++ b/internal/config/constants.go @@ -0,0 +1,6 @@ +package config + +const ( + DefaultConfigPath = "software.txt" + DefaultGiteaServer = "https://git.quad4.io" +) diff --git a/internal/config/updater.go b/internal/config/updater.go new file mode 100644 index 0000000..0103c3d --- /dev/null +++ b/internal/config/updater.go @@ -0,0 +1,30 @@ +package config + +import ( + "log" + "sync" + "time" + + "software-station/internal/models" +) + +func StartBackgroundUpdater(path, server, token string, mu *sync.RWMutex, softwareList *[]models.Software, interval time.Duration) { + ticker := time.NewTicker(interval) + go func() { + for range ticker.C { + log.Println("Checking for software updates...") + newList := LoadSoftwareFromGitea(path, server, token) + if len(newList) > 0 { + mu.Lock() + *softwareList = newList + mu.Unlock() + log.Printf("Software list updated with %d items", len(newList)) + } + } + }() +} + +// LoadSoftwareFromGitea always fetches from Gitea and updates cache +func LoadSoftwareFromGitea(path, server, token string) []models.Software { + return LoadSoftwareExtended(path, server, token, false) +} diff --git a/internal/gitea/client.go b/internal/gitea/client.go new file mode 100644 index 0000000..db4616d --- /dev/null +++ b/internal/gitea/client.go @@ -0,0 +1,298 @@ +package gitea + +import ( + "bufio" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "software-station/internal/models" +) + +func DetectOS(filename string) string { + lower := strings.ToLower(filename) + + osMap := []struct { + patterns []string + suffixes []string + os string + }{ + { + patterns: []string{"windows"}, + suffixes: []string{".exe", ".msi"}, + os: models.OSWindows, + }, + { + patterns: []string{"linux"}, + suffixes: []string{".deb", ".rpm", ".appimage", ".flatpak"}, + os: models.OSLinux, + }, + { + patterns: []string{"mac", "darwin"}, + suffixes: []string{".dmg", ".pkg"}, + os: models.OSMacOS, + }, + { + patterns: []string{"freebsd"}, + os: models.OSFreeBSD, + }, + { + patterns: []string{"openbsd"}, + os: models.OSOpenBSD, + }, + { + patterns: []string{"android"}, + suffixes: []string{".apk"}, + os: models.OSAndroid, + }, + { + patterns: []string{"arm", "aarch64"}, + os: models.OSARM, + }, + } + + for _, entry := range osMap { + for _, p := range entry.patterns { + if strings.Contains(lower, p) { + return entry.os + } + } + for _, s := range entry.suffixes { + if strings.HasSuffix(lower, s) { + return entry.os + } + } + } + + return models.OSUnknown +} + +func FetchRepoInfo(server, token, owner, repo string) (string, []string, string, bool, string, error) { + url := fmt.Sprintf("%s%s/%s/%s", server, RepoAPIPath, owner, repo) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", nil, "", false, "", err + } + + if token != "" { + req.Header.Set("Authorization", "token "+token) + } + + client := &http.Client{Timeout: DefaultTimeout} + resp, err := client.Do(req) + if err != nil { + return "", nil, "", false, "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", nil, "", false, "", fmt.Errorf("gitea api returned status %d", resp.StatusCode) + } + + var info struct { + Description string `json:"description"` + Topics []string `json:"topics"` + DefaultBranch string `json:"default_branch"` + Licenses []string `json:"licenses"` + Private bool `json:"private"` + AvatarURL string `json:"avatar_url"` + } + if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { + return "", nil, "", false, "", err + } + + license := "" + if len(info.Licenses) > 0 { + license = info.Licenses[0] + } + if license == "" { + // Try to detect license from file if API returns nothing + license = detectLicenseFromFile(server, token, owner, repo, info.DefaultBranch) + } + + return info.Description, info.Topics, license, info.Private, info.AvatarURL, nil +} + +func detectLicenseFromFile(server, token, owner, repo, defaultBranch string) string { + branches := []string{"main", "master"} + if defaultBranch != "" { + // Put default branch first + branches = append([]string{defaultBranch}, "main", "master") + } + // Deduplicate + seen := make(map[string]bool) + var finalBranches []string + for _, b := range branches { + if !seen[b] { + seen[b] = true + finalBranches = append(finalBranches, b) + } + } + + for _, branch := range finalBranches { + url := fmt.Sprintf("%s/%s/%s/raw/branch/%s/LICENSE", server, owner, repo, branch) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + continue + } + if token != "" { + req.Header.Set("Authorization", "token "+token) + } + + client := &http.Client{Timeout: DefaultTimeout} + resp, err := client.Do(req) + if err != nil { + continue + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + // Read first few lines to guess license + scanner := bufio.NewScanner(resp.Body) + for i := 0; i < 5 && scanner.Scan(); i++ { + line := strings.ToUpper(scanner.Text()) + if strings.Contains(line, "MIT LICENSE") { + return "MIT" + } + if strings.Contains(line, "GNU GENERAL PUBLIC LICENSE") || strings.Contains(line, "GPL") { + return "GPL" + } + if strings.Contains(line, "APACHE LICENSE") { + return "Apache-2.0" + } + if strings.Contains(line, "BSD") { + return "BSD" + } + } + return "LICENSE" // Found file but couldn't detect type + } + } + return "" +} + +func IsSBOM(filename string) bool { + lower := strings.ToLower(filename) + return strings.Contains(lower, "sbom") || + strings.Contains(lower, "cyclonedx") || + strings.Contains(lower, "spdx") || + strings.HasSuffix(lower, ".cdx.json") || + strings.HasSuffix(lower, ".spdx.json") +} + +func FetchReleases(server, token, owner, repo string) ([]models.Release, error) { + url := fmt.Sprintf("%s%s/%s/%s%s", server, RepoAPIPath, owner, repo, ReleasesSuffix) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + if token != "" { + req.Header.Set("Authorization", "token "+token) + } + + client := &http.Client{Timeout: DefaultTimeout} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("gitea api returned status %d", resp.StatusCode) + } + + var giteaReleases []struct { + TagName string `json:"tag_name"` + Body string `json:"body"` + CreatedAt time.Time `json:"created_at"` + Assets []struct { + Name string `json:"name"` + Size int64 `json:"size"` + URL string `json:"browser_download_url"` + } `json:"assets"` + } + + if err := json.NewDecoder(resp.Body).Decode(&giteaReleases); err != nil { + return nil, err + } + + var releases []models.Release + for _, gr := range giteaReleases { + var assets []models.Asset + var checksumsURL string + + // First pass: identify assets and look for checksum file + for _, ga := range gr.Assets { + if ga.Name == "SHA256SUMS" { + checksumsURL = ga.URL + continue + } + assets = append(assets, models.Asset{ + Name: ga.Name, + Size: ga.Size, + URL: ga.URL, + OS: DetectOS(ga.Name), + IsSBOM: IsSBOM(ga.Name), + }) + } + + // Second pass: if checksum file exists, fetch and parse it + if checksumsURL != "" { + checksums, err := fetchAndParseChecksums(checksumsURL, token) + if err == nil { + for i := range assets { + if sha, ok := checksums[assets[i].Name]; ok { + assets[i].SHA256 = sha + } + } + } + } + + releases = append(releases, models.Release{ + TagName: gr.TagName, + Body: gr.Body, + CreatedAt: gr.CreatedAt, + Assets: assets, + }) + } + + return releases, nil +} + +func fetchAndParseChecksums(url, token string) (map[string]string, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + if token != "" { + req.Header.Set("Authorization", "token "+token) + } + + client := &http.Client{Timeout: DefaultTimeout} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch checksums: %d", resp.StatusCode) + } + + checksums := make(map[string]string) + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + parts := strings.Fields(line) + if len(parts) >= 2 { + // Format is usually: hash filename + checksums[parts[1]] = parts[0] + } + } + return checksums, nil +} diff --git a/internal/gitea/client_test.go b/internal/gitea/client_test.go new file mode 100644 index 0000000..97dd943 --- /dev/null +++ b/internal/gitea/client_test.go @@ -0,0 +1,118 @@ +package gitea + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "software-station/internal/models" +) + +func TestFetchRepoInfo(t *testing.T) { + mockSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "token test-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + w.Write([]byte(`{"description": "Test Repo", "topics": ["test"], "licenses": ["MIT"], "private": false, "avatar_url": "https://example.com/avatar.png"}`)) + })) + defer mockSrv.Close() + + desc, topics, license, isPrivate, avatarURL, err := FetchRepoInfo(mockSrv.URL, "test-token", "owner", "repo") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if desc != "Test Repo" || len(topics) != 1 || topics[0] != "test" || license != "MIT" || isPrivate || avatarURL != "https://example.com/avatar.png" { + t.Errorf("unexpected results: %s, %v, %s, %v, %s", desc, topics, license, isPrivate, avatarURL) + } + + _, _, _, _, _, err = FetchRepoInfo(mockSrv.URL, "wrong-token", "owner", "repo") + if err == nil { + t.Error("expected error for unauthorized request") + } +} + +func TestFetchReleases(t *testing.T) { + var srv *httptest.Server + srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.Path, "releases") { + w.Write([]byte(fmt.Sprintf(`[{"tag_name": "v1.0.0", "assets": [{"name": "test.exe", "size": 100, "browser_download_url": "%s/test.exe"}, {"name": "SHA256SUMS", "size": 50, "browser_download_url": "%s/SHA256SUMS"}]}]`, srv.URL, srv.URL))) + } else if strings.Contains(r.URL.Path, "SHA256SUMS") { + w.Write([]byte(`hash123 test.exe`)) + } + })) + defer srv.Close() + + releases, err := FetchReleases(srv.URL, "", "owner", "repo") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(releases) != 1 || releases[0].TagName != "v1.0.0" { + t.Errorf("unexpected releases: %v", releases) + } + if len(releases[0].Assets) != 1 || releases[0].Assets[0].Name != "test.exe" || releases[0].Assets[0].SHA256 != "hash123" { + t.Errorf("unexpected assets: %v", releases[0].Assets) + } +} + +func TestDetectOS(t *testing.T) { + tests := []struct { + filename string + expected string + }{ + {"app.exe", models.OSWindows}, + {"app.msi", models.OSWindows}, + {"app_linux", models.OSLinux}, + {"app.deb", models.OSLinux}, + {"app.rpm", models.OSLinux}, + {"app.dmg", models.OSMacOS}, + {"app.pkg", models.OSMacOS}, + {"app_freebsd", models.OSFreeBSD}, + {"app_openbsd", models.OSOpenBSD}, + {"app.apk", models.OSAndroid}, + {"app_arm64", models.OSARM}, + {"app_aarch64", models.OSARM}, + {"unknown", models.OSUnknown}, + } + + for _, tt := range tests { + got := DetectOS(tt.filename) + if got != tt.expected { + t.Errorf("DetectOS(%s) = %s, expected %s", tt.filename, got, tt.expected) + } + } +} + +func TestIsSBOM(t *testing.T) { + tests := []struct { + filename string + expected bool + }{ + {"sbom.json", true}, + {"cyclonedx.json", true}, + {"spdx.json", true}, + {"app.exe", false}, + {"app.cdx.json", true}, + {"app.spdx.json", true}, + } + + for _, tt := range tests { + if got := IsSBOM(tt.filename); got != tt.expected { + t.Errorf("IsSBOM(%s) = %v, expected %v", tt.filename, got, tt.expected) + } + } +} + +func TestFetchAndParseChecksumsError(t *testing.T) { + mockSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer mockSrv.Close() + + _, err := fetchAndParseChecksums(mockSrv.URL, "") + if err == nil { + t.Error("expected error for 404") + } +} diff --git a/internal/gitea/constants.go b/internal/gitea/constants.go new file mode 100644 index 0000000..0755ad0 --- /dev/null +++ b/internal/gitea/constants.go @@ -0,0 +1,9 @@ +package gitea + +import "time" + +const ( + DefaultTimeout = 10 * time.Second + RepoAPIPath = "/api/v1/repos" + ReleasesSuffix = "/releases" +) diff --git a/internal/models/constants.go b/internal/models/constants.go new file mode 100644 index 0000000..34ec271 --- /dev/null +++ b/internal/models/constants.go @@ -0,0 +1,12 @@ +package models + +const ( + OSWindows = "windows" + OSLinux = "linux" + OSMacOS = "macos" + OSFreeBSD = "freebsd" + OSOpenBSD = "openbsd" + OSAndroid = "android" + OSARM = "arm" + OSUnknown = "unknown" +) diff --git a/internal/models/models.go b/internal/models/models.go new file mode 100644 index 0000000..0cd90d3 --- /dev/null +++ b/internal/models/models.go @@ -0,0 +1,41 @@ +package models + +import "time" + +type Asset struct { + Name string `json:"name"` + Size int64 `json:"size"` + URL string `json:"url"` + OS string `json:"os"` + SHA256 string `json:"sha256,omitempty"` + IsSBOM bool `json:"is_sbom"` +} + +type Release struct { + TagName string `json:"tag_name"` + Body string `json:"body,omitempty"` + CreatedAt time.Time `json:"created_at"` + Assets []Asset `json:"assets"` +} + +type Software struct { + Name string `json:"name"` + Owner string `json:"owner"` + Description string `json:"description"` + Releases []Release `json:"releases"` + GiteaURL string `json:"gitea_url"` + Topics []string `json:"topics"` + License string `json:"license,omitempty"` + IsPrivate bool `json:"is_private"` + AvatarURL string `json:"avatar_url,omitempty"` +} + +type FingerprintData struct { + Known bool `json:"known"` + TotalBytes int64 `json:"total_bytes"` +} + +type SoftwareResponse struct { + GiteaURL string `json:"gitea_url"` + Software []Software `json:"software"` +} diff --git a/internal/security/constants.go b/internal/security/constants.go new file mode 100644 index 0000000..f98ec27 --- /dev/null +++ b/internal/security/constants.go @@ -0,0 +1,50 @@ +package security + +import ( + "time" + + "golang.org/x/time/rate" +) + +const ( + _ = iota + KB = 1 << (10 * iota) + MB + GB +) + +const ( + // Download Throttling + DefaultDownloadLimit = rate.Limit(5 * MB) // 5MB/s + DefaultDownloadBurst = 2 * MB // 2MB + + SpeedDownloaderLimit = rate.Limit(1 * MB) // 1MB/s + SpeedDownloaderBurst = 512 * KB // 512KB + + HeavyDownloaderThreshold = 1 * GB // 1GB + HeavyDownloaderLimit = rate.Limit(256 * KB) // 256KB/s + + // Rate Limiting + GlobalRateLimit = 100 + GlobalRateWindow = 1 * time.Minute + APIRateLimit = 30 + APIRateWindow = 1 * time.Minute +) + +var ForbiddenPatterns = []string{ + ".git", ".env", ".aws", ".config", ".ssh", + "wp-admin", "wp-login", "phpinfo", ".php", + "etc/passwd", "cgi-bin", "shell", "cmd", + ".sql", ".bak", ".old", ".zip", ".rar", +} + +var BotUserAgents = []string{ + "bot", "crawl", "spider", "slurp", "googlebot", "bingbot", "yandexbot", + "ahrefsbot", "baiduspider", "duckduckbot", "facebookexternalhit", + "twitterbot", "rogerbot", "linkedinbot", "embedly", "quora link preview", + "showyoubot", "outbrain", "pinterest", "slackbot", "vkShare", "W3C_Validator", +} + +var SpeedDownloaders = []string{ + "aria2", "wget", "curl", "axel", "transmission", "libcurl", +} diff --git a/internal/security/security.go b/internal/security/security.go new file mode 100644 index 0000000..fc2b065 --- /dev/null +++ b/internal/security/security.go @@ -0,0 +1,220 @@ +package security + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "strings" + "sync/atomic" + "syscall" + "time" + + "software-station/internal/models" + "software-station/internal/stats" + + "golang.org/x/time/rate" +) + +type ThrottledReader struct { + R io.ReadCloser + Limiter *rate.Limiter + Fingerprint string + Stats *stats.Service +} + +func (tr *ThrottledReader) Read(p []byte) (n int, err error) { + n, err = tr.R.Read(p) + if n > 0 && tr.Limiter != nil && tr.Stats != nil { + tr.Stats.KnownHashes.RLock() + data, exists := tr.Stats.KnownHashes.Data[tr.Fingerprint] + tr.Stats.KnownHashes.RUnlock() + + var total int64 + if exists { + total = atomic.AddInt64(&data.TotalBytes, int64(n)) + } + atomic.AddInt64(&tr.Stats.GlobalStats.TotalBytes, int64(n)) + + if total > HeavyDownloaderThreshold { + tr.Limiter.SetLimit(HeavyDownloaderLimit) + } + + if err := tr.Limiter.WaitN(context.Background(), n); err != nil { + return n, err + } + } + return n, err +} + +func (tr *ThrottledReader) Close() error { + return tr.R.Close() +} + +type contextKey string + +const FingerprintKey contextKey = "fingerprint" + +func GetRequestFingerprint(r *http.Request, s *stats.Service) string { + if f, ok := r.Context().Value(FingerprintKey).(string); ok { + return f + } + + remoteAddr := r.RemoteAddr + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + if comma := strings.IndexByte(xff, ','); comma != -1 { + remoteAddr = strings.TrimSpace(xff[:comma]) + } else { + remoteAddr = strings.TrimSpace(xff) + } + } else if xri := r.Header.Get("X-Real-IP"); xri != "" { + remoteAddr = strings.TrimSpace(xri) + } + + ipStr, _, err := net.SplitHostPort(remoteAddr) + if err != nil { + ipStr = remoteAddr + } + + ip := net.ParseIP(ipStr) + if ip != nil { + if ip.To4() == nil { + ip = ip.Mask(net.CIDRMask(64, 128)) + } + ipStr = ip.String() + } + + ua := r.Header.Get("User-Agent") + hash := sha256.New() + hash.Write([]byte(ipStr + ua)) + fingerprint := hex.EncodeToString(hash.Sum(nil)) + + s.KnownHashes.Lock() + if _, exists := s.KnownHashes.Data[fingerprint]; !exists { + s.KnownHashes.Data[fingerprint] = &models.FingerprintData{ + Known: true, + } + s.SaveHashes() + } + s.KnownHashes.Unlock() + + return fingerprint +} + +func IsPrivateIP(ip net.IP) bool { + if os.Getenv("ALLOW_LOOPBACK") == "true" && ip.IsLoopback() { + return false + } + if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() { + return true + } + + // Private IP ranges + privateRanges := []struct { + start net.IP + end net.IP + }{ + {net.ParseIP("10.0.0.0"), net.ParseIP("10.255.255.255")}, + {net.ParseIP("172.16.0.0"), net.ParseIP("172.31.255.255")}, + {net.ParseIP("192.168.0.0"), net.ParseIP("192.168.255.255")}, + {net.ParseIP("fd00::"), net.ParseIP("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")}, + } + + for _, r := range privateRanges { + if bytes.Compare(ip, r.start) >= 0 && bytes.Compare(ip, r.end) <= 0 { + return true + } + } + + return false +} + +func GetSafeHTTPClient(timeout time.Duration) *http.Client { + dialer := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + Control: func(network, address string, c syscall.RawConn) error { + host, _, err := net.SplitHostPort(address) + if err != nil { + return err + } + ip := net.ParseIP(host) + if ip != nil && IsPrivateIP(ip) { + return fmt.Errorf("SSRF protection: forbidden IP %s", ip) + } + return nil + }, + } + + transport := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: dialer.DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + + return &http.Client{ + Transport: transport, + Timeout: timeout, + } +} + +func SecurityMiddleware(s *stats.Service) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + path := strings.ToLower(r.URL.Path) + ua := strings.ToLower(r.UserAgent()) + fingerprint := GetRequestFingerprint(r, s) + + ctx := context.WithValue(r.Context(), FingerprintKey, fingerprint) + r = r.WithContext(ctx) + + s.GlobalStats.Lock() + s.GlobalStats.UniqueRequests[fingerprint] = true + if !strings.HasPrefix(path, "/api") { + s.GlobalStats.WebRequests[fingerprint] = true + } + s.GlobalStats.Unlock() + + defer func() { + s.GlobalStats.Lock() + s.GlobalStats.TotalResponseTime += time.Since(start) + s.GlobalStats.TotalRequests++ + s.GlobalStats.Unlock() + }() + + for _, bot := range BotUserAgents { + if strings.Contains(ua, bot) { + s.GlobalStats.Lock() + s.GlobalStats.BlockedRequests[fingerprint] = true + s.GlobalStats.Unlock() + http.Error(w, "Bots are not allowed", http.StatusForbidden) + return + } + } + + for _, pattern := range ForbiddenPatterns { + if strings.Contains(path, pattern) { + s.GlobalStats.Lock() + s.GlobalStats.BlockedRequests[fingerprint] = true + s.GlobalStats.Unlock() + log.Printf("Blocked suspicious request: %s from %s (%s)", r.URL.String(), r.RemoteAddr, r.UserAgent()) + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + } + + next.ServeHTTP(w, r) + }) + } +} diff --git a/internal/security/security_test.go b/internal/security/security_test.go new file mode 100644 index 0000000..b3d94b9 --- /dev/null +++ b/internal/security/security_test.go @@ -0,0 +1,176 @@ +package security + +import ( + "bytes" + "io" + "net" + "net/http" + "net/http/httptest" + "software-station/internal/models" + "software-station/internal/stats" + "strings" + "testing" + "time" + + "golang.org/x/time/rate" +) + +func TestThrottledReader(t *testing.T) { + content := []byte("hello world") + inner := io.NopCloser(bytes.NewReader(content)) + limiter := rate.NewLimiter(rate.Limit(100), 100) + fp := "test-fp" + + statsService := stats.NewService("test-hashes.json") + statsService.KnownHashes.Lock() + statsService.KnownHashes.Data[fp] = &models.FingerprintData{Known: true} + statsService.KnownHashes.Unlock() + + tr := &ThrottledReader{ + R: inner, + Limiter: limiter, + Fingerprint: fp, + Stats: statsService, + } + + p := make([]byte, 5) + n, err := tr.Read(p) + if err != nil { + t.Fatalf("Read failed: %v", err) + } + if n != 5 { + t.Errorf("expected 5 bytes, got %d", n) + } + + statsService.KnownHashes.RLock() + data, ok := statsService.KnownHashes.Data["test-fp"] + statsService.KnownHashes.RUnlock() + if !ok { + t.Fatal("fingerprint data not found") + } + total := data.TotalBytes + if total != 5 { + t.Errorf("expected 5 bytes in stats, got %d", total) + } + + tr.Close() +} + +func TestGetRequestFingerprint(t *testing.T) { + statsService := stats.NewService("test-hashes.json") + + // IPv4 + req1 := httptest.NewRequest("GET", "/", nil) + req1.RemoteAddr = "1.2.3.4:1234" + f1 := GetRequestFingerprint(req1, statsService) + + req2 := httptest.NewRequest("GET", "/", nil) + req2.RemoteAddr = "1.2.3.4:5678" + f2 := GetRequestFingerprint(req2, statsService) + if f1 != f2 { + t.Error("fingerprints should match for same IPv4") + } + + // X-Forwarded-For + req3 := httptest.NewRequest("GET", "/", nil) + req3.Header.Set("X-Forwarded-For", "5.6.7.8, 1.2.3.4") + f3 := GetRequestFingerprint(req3, statsService) + if f1 == f3 { + t.Error("fingerprints should differ for different IPs") + } + + // IPv6 masking + req4 := httptest.NewRequest("GET", "/", nil) + req4.RemoteAddr = "[2001:db8::1]:1234" + f4 := GetRequestFingerprint(req4, statsService) + + req5 := httptest.NewRequest("GET", "/", nil) + req5.RemoteAddr = "[2001:db8::2]:1234" + f5 := GetRequestFingerprint(req5, statsService) + if f4 != f5 { + t.Error("fingerprints should match for same IPv6 /64 prefix") + } +} + +func TestSecurityMiddleware(t *testing.T) { + statsService := stats.NewService("test-hashes.json") + handler := SecurityMiddleware(statsService)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + // Test bot blocking + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set("User-Agent", "Googlebot") + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + if rr.Code != http.StatusForbidden { + t.Errorf("expected 403 for bot, got %d", rr.Code) + } + + // Test forbidden pattern + req = httptest.NewRequest("GET", "/.git/config", nil) + rr = httptest.NewRecorder() + handler.ServeHTTP(rr, req) + if rr.Code != http.StatusForbidden { + t.Errorf("expected 403 for forbidden pattern, got %d", rr.Code) + } + + // Test normal request + req = httptest.NewRequest("GET", "/api/software", nil) + rr = httptest.NewRecorder() + handler.ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + t.Errorf("expected 200 for normal request, got %d", rr.Code) + } +} + +func TestIsPrivateIP_Extended(t *testing.T) { + tests := []struct { + ip string + isPrivate bool + }{ + {"127.0.0.1", true}, + {"10.0.0.1", true}, + {"172.16.0.1", true}, + {"192.168.1.1", true}, + {"0.0.0.0", true}, + {"::1", true}, + {"::", true}, + {"fd00::1", true}, + {"8.8.8.8", false}, + {"1.1.1.1", false}, + } + + for _, tt := range tests { + ip := net.ParseIP(tt.ip) + if got := IsPrivateIP(ip); got != tt.isPrivate { + t.Errorf("IsPrivateIP(%s) = %v; want %v", tt.ip, got, tt.isPrivate) + } + } +} + +func TestGetSafeHTTPClient_BlocksPrivateIPs(t *testing.T) { + client := GetSafeHTTPClient(1 * time.Second) + + // Test 127.0.0.1 + _, err := client.Get("http://127.0.0.1:12345") + if err == nil { + t.Error("Expected error for 127.0.0.1, got nil") + } else if !strings.Contains(err.Error(), "SSRF protection") { + t.Logf("Got error for 127.0.0.1: %v", err) + if !strings.Contains(err.Error(), "SSRF protection") { + t.Errorf("Expected 'SSRF protection' error, got: %v", err) + } + } + + // Test 0.0.0.0 + _, err = client.Get("http://0.0.0.0:12345") + if err == nil { + t.Error("Expected error for 0.0.0.0, got nil") + } else if !strings.Contains(err.Error(), "SSRF protection") { + t.Logf("Got error for 0.0.0.0: %v", err) + if !strings.Contains(err.Error(), "SSRF protection") { + t.Errorf("Expected 'SSRF protection' error, got: %v", err) + } + } +} diff --git a/internal/stats/constants.go b/internal/stats/constants.go new file mode 100644 index 0000000..efd44cd --- /dev/null +++ b/internal/stats/constants.go @@ -0,0 +1,5 @@ +package stats + +const ( + DefaultHashesFile = "hashes.json" +) diff --git a/internal/stats/stats.go b/internal/stats/stats.go new file mode 100644 index 0000000..4efaa9c --- /dev/null +++ b/internal/stats/stats.go @@ -0,0 +1,154 @@ +package stats + +import ( + "encoding/json" + "log" + "net/http" + "os" + "sync" + "sync/atomic" + "time" + + "software-station/internal/models" + + "golang.org/x/time/rate" +) + +type Service struct { + HashesFile string + KnownHashes struct { + sync.RWMutex + Data map[string]*models.FingerprintData + } + GlobalStats struct { + sync.RWMutex + UniqueRequests map[string]bool + SuccessDownloads map[string]bool + BlockedRequests map[string]bool + LimitedRequests map[string]bool + WebRequests map[string]bool + TotalResponseTime time.Duration + TotalRequests int64 + TotalBytes int64 + StartTime time.Time + } + DownloadStats struct { + sync.RWMutex + Limiters map[string]*rate.Limiter + } + hashesDirty int32 + stopChan chan struct{} +} + +func NewService(hashesFile string) *Service { + s := &Service{ + HashesFile: hashesFile, + stopChan: make(chan struct{}), + } + s.KnownHashes.Data = make(map[string]*models.FingerprintData) + s.GlobalStats.UniqueRequests = make(map[string]bool) + s.GlobalStats.SuccessDownloads = make(map[string]bool) + s.GlobalStats.BlockedRequests = make(map[string]bool) + s.GlobalStats.LimitedRequests = make(map[string]bool) + s.GlobalStats.WebRequests = make(map[string]bool) + s.GlobalStats.StartTime = time.Now() + s.DownloadStats.Limiters = make(map[string]*rate.Limiter) + return s +} + +func (s *Service) Start() { + go func() { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if atomic.CompareAndSwapInt32(&s.hashesDirty, 1, 0) { + s.FlushHashes() + } + case <-s.stopChan: + s.FlushHashes() + return + } + } + }() +} + +func (s *Service) Stop() { + close(s.stopChan) +} + +func (s *Service) LoadHashes() { + data, err := os.ReadFile(s.HashesFile) + if err != nil { + if !os.IsNotExist(err) { + log.Printf("Error reading hashes file: %v", err) + } + return + } + s.KnownHashes.Lock() + defer s.KnownHashes.Unlock() + if err := json.Unmarshal(data, &s.KnownHashes.Data); err != nil { + log.Printf("Error unmarshaling hashes: %v", err) + return + } + + var total int64 + for _, d := range s.KnownHashes.Data { + total += atomic.LoadInt64(&d.TotalBytes) + } + atomic.StoreInt64(&s.GlobalStats.TotalBytes, total) +} + +func (s *Service) SaveHashes() { + atomic.StoreInt32(&s.hashesDirty, 1) +} + +func (s *Service) FlushHashes() { + s.KnownHashes.RLock() + data, err := json.MarshalIndent(s.KnownHashes.Data, "", " ") + s.KnownHashes.RUnlock() + if err != nil { + log.Printf("Error marshaling hashes: %v", err) + return + } + if err := os.WriteFile(s.HashesFile, data, 0600); err != nil { + log.Printf("Error writing hashes file: %v", err) + } +} + +func (s *Service) APIStatsHandler(w http.ResponseWriter, r *http.Request) { + s.GlobalStats.RLock() + defer s.GlobalStats.RUnlock() + + avgResponse := time.Duration(0) + if s.GlobalStats.TotalRequests > 0 { + avgResponse = s.GlobalStats.TotalResponseTime / time.Duration(s.GlobalStats.TotalRequests) + } + + totalBytes := atomic.LoadInt64(&s.GlobalStats.TotalBytes) + uptime := time.Since(s.GlobalStats.StartTime) + avgSpeed := float64(totalBytes) / uptime.Seconds() + + status := "healthy" + if s.GlobalStats.TotalRequests > 0 && float64(len(s.GlobalStats.BlockedRequests))/float64(s.GlobalStats.TotalRequests) > 0.5 { + status = "unhealthy" + } + + data := map[string]interface{}{ + "total_unique_download_requests": len(s.GlobalStats.UniqueRequests), + "total_unique_success_downloads": len(s.GlobalStats.SuccessDownloads), + "total_unique_blocked": len(s.GlobalStats.BlockedRequests), + "total_unique_limited": len(s.GlobalStats.LimitedRequests), + "total_unique_web_requests": len(s.GlobalStats.WebRequests), + "avg_speed_bps": avgSpeed, + "avg_response_time": avgResponse.String(), + "uptime": uptime.String(), + "status": status, + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(data); err != nil { + log.Printf("Error encoding stats: %v", err) + } +} diff --git a/internal/stats/stats_test.go b/internal/stats/stats_test.go new file mode 100644 index 0000000..ec3e2b4 --- /dev/null +++ b/internal/stats/stats_test.go @@ -0,0 +1,54 @@ +package stats + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "software-station/internal/models" + "testing" +) + +func TestStats(t *testing.T) { + tempFile := "test_hashes.json" + defer os.Remove(tempFile) + + service := NewService(tempFile) + + // Test SaveHashes + service.KnownHashes.Lock() + service.KnownHashes.Data["test"] = &models.FingerprintData{Known: true, TotalBytes: 100} + service.KnownHashes.Unlock() + service.FlushHashes() + + if _, err := os.Stat(tempFile); os.IsNotExist(err) { + t.Error("SaveHashes did not create file") + } + + // Test LoadHashes + service.KnownHashes.Lock() + delete(service.KnownHashes.Data, "test") + service.KnownHashes.Unlock() + service.LoadHashes() + + service.KnownHashes.RLock() + if data, ok := service.KnownHashes.Data["test"]; !ok || data.TotalBytes != 100 { + t.Errorf("LoadHashes did not restore data correctly: %+v", data) + } + service.KnownHashes.RUnlock() + + // Test APIStatsHandler + req := httptest.NewRequest("GET", "/api/stats", nil) + rr := httptest.NewRecorder() + service.APIStatsHandler(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rr.Code) + } + + var stats map[string]interface{} + json.Unmarshal(rr.Body.Bytes(), &stats) + if stats["status"] != "healthy" { + t.Errorf("expected healthy status, got %v", stats["status"]) + } +} diff --git a/legal/disclaimer.txt b/legal/disclaimer.txt new file mode 100644 index 0000000..b5878e9 --- /dev/null +++ b/legal/disclaimer.txt @@ -0,0 +1,6 @@ +Legal Disclaimer + +The software distributed through this platform is provided "as-is" without any express or implied warranties. Quad4 and its contributors shall not be held liable for any damages arising from the use of the software downloaded from this station. + +Please verify the SHA256 checksums provided for each asset to ensure integrity. + diff --git a/legal/privacy.txt b/legal/privacy.txt new file mode 100644 index 0000000..e8f41c2 --- /dev/null +++ b/legal/privacy.txt @@ -0,0 +1,10 @@ +Privacy Policy + +We value your privacy. This software distribution station does not track individual users or collect personal data beyond what is strictly necessary for the technical operation of the service (e.g., rate limiting, download stats based on IP fingerprints). + +Data Collection: +- IP Address (hashed/fingerprinted for security and rate limiting) +- User Agent (for bot detection and OS-specific download stats) + +We do not use cookies or third-party trackers. + diff --git a/legal/terms.txt b/legal/terms.txt new file mode 100644 index 0000000..e5f682e --- /dev/null +++ b/legal/terms.txt @@ -0,0 +1,10 @@ +Terms of Service + +By using this software station, you agree to the following terms: + +1. Fair Use: You will not attempt to bypass rate limits or scrap the station excessively. +2. Distribution: The software provided here is mirrored from Gitea. Licenses for individual projects apply. +3. Disclaimer: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + +We reserve the right to block any fingerprint or IP that engages in malicious activity. + diff --git a/main.go b/main.go new file mode 100644 index 0000000..a39c742 --- /dev/null +++ b/main.go @@ -0,0 +1,166 @@ +package main + +import ( + "context" + "embed" + "flag" + "fmt" + "io/fs" + "log" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "software-station/internal/api" + "software-station/internal/config" + "software-station/internal/security" + "software-station/internal/stats" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" + "github.com/go-chi/httprate" + "github.com/unrolled/secure" +) + +//go:embed all:frontend/build +var frontendBuild embed.FS + +var ( + giteaToken string + giteaServer string + configPath string +) + +func main() { + flag.StringVar(&giteaToken, "t", os.Getenv("GITEA_TOKEN"), "Gitea API Token") + flag.StringVar(&giteaServer, "s", "https://git.quad4.io", "Gitea Server URL") + flag.StringVar(&configPath, "c", config.DefaultConfigPath, "Path to software.txt (local or remote)") + port := flag.String("p", "8080", "Server port") + isProd := flag.Bool("prod", os.Getenv("NODE_ENV") == "production", "Run in production mode") + updateInterval := flag.Duration("u", 1*time.Hour, "Software update interval") + flag.Parse() + + statsService := stats.NewService(stats.DefaultHashesFile) + statsService.LoadHashes() + statsService.Start() + defer statsService.Stop() + + initialSoftware := config.LoadSoftware(configPath, giteaServer, giteaToken) + apiServer := api.NewServer(giteaToken, initialSoftware, statsService) + config.StartBackgroundUpdater(configPath, giteaServer, giteaToken, apiServer.SoftwareList.GetLock(), apiServer.SoftwareList.GetDataPtr(), *updateInterval) + + r := chi.NewRouter() + + r.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "X-Account-Number"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: true, + MaxAge: 300, + })) + + secureMiddleware := secure.New(secure.Options{ + FrameDeny: true, + ContentTypeNosniff: true, + BrowserXssFilter: true, + ContentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self';", + IsDevelopment: !*isProd, + }) + r.Use(secureMiddleware.Handler) + + r.Use(middleware.RealIP) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + r.Use(middleware.Compress(api.CompressionLevel)) + r.Use(security.SecurityMiddleware(statsService)) + + r.Use(httprate.Limit( + security.GlobalRateLimit, + security.GlobalRateWindow, + httprate.WithKeyFuncs(func(r *http.Request) (string, error) { + return security.GetRequestFingerprint(r, statsService), nil + }), + )) + + r.Route("/api", func(r chi.Router) { + r.Use(httprate.Limit( + security.APIRateLimit, + security.APIRateWindow, + httprate.WithKeyFuncs(func(r *http.Request) (string, error) { + return security.GetRequestFingerprint(r, statsService), nil + }), + )) + r.Get("/software", apiServer.APISoftwareHandler) + r.Get("/download", apiServer.DownloadProxyHandler) + r.Get("/avatar", apiServer.AvatarHandler) + r.Get("/stats", statsService.APIStatsHandler) + r.Get("/legal", apiServer.LegalHandler) + r.Get("/rss", apiServer.RSSHandler) + }) + + contentStatic, err := fs.Sub(frontendBuild, "frontend/build") + if err != nil { + log.Fatal(err) + } + staticHandler := http.FileServer(http.FS(contentStatic)) + + r.Get("/*", func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + if path == "/" { + path = "index.html" + } else { + path = strings.TrimPrefix(path, "/") + } + + f, err := contentStatic.Open(path) + if err != nil { + indexData, err := fs.ReadFile(contentStatic, "index.html") + if err != nil { + http.Error(w, "Index not found", http.StatusInternalServerError) + return + } + http.ServeContent(w, r, "index.html", time.Unix(0, 0), strings.NewReader(string(indexData))) + return + } + if err := f.Close(); err != nil { + log.Printf("Error closing static file: %v", err) + } + + staticHandler.ServeHTTP(w, r) + }) + + server := &http.Server{ + Addr: ":" + *port, + ReadTimeout: 15 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 120 * time.Second, + Handler: r, + } + + done := make(chan os.Signal, 1) + signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + go func() { + fmt.Printf("Server starting on http://localhost:%s (Gitea: %s, Prod: %v)\n", *port, giteaServer, *isProd) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Printf("Listen error: %s\n", err) + done <- syscall.SIGTERM + } + }() + + <-done + fmt.Println("\nServer stopping...") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + log.Fatalf("Server shutdown failed: %+v", err) + } + fmt.Println("Server exited properly") +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..26424b6 --- /dev/null +++ b/main_test.go @@ -0,0 +1,278 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "software-station/internal/api" + "software-station/internal/config" + "software-station/internal/gitea" + "software-station/internal/models" + "software-station/internal/security" + "software-station/internal/stats" + + "github.com/go-chi/chi/v5" +) + +func TestMainHandlers(t *testing.T) { + os.Setenv("ALLOW_LOOPBACK", "true") + defer os.Unsetenv("ALLOW_LOOPBACK") + + // Setup mock software.txt + configPath = "test_software.txt" + os.WriteFile(configPath, []byte("Quad4-Software/software-station"), 0644) + defer os.Remove(configPath) + defer os.Remove("hashes.json") + os.RemoveAll(".cache") // Clear cache for tests + + // Mock Gitea Server + var mockGitea *httptest.Server + mockGitea = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.Path, "releases") { + w.Write([]byte(fmt.Sprintf(`[{"tag_name": "v1.0.0", "body": "Release notes for v1.0.0", "created_at": "2025-12-27T10:00:00Z", "assets": [{"name": "test.exe", "size": 100, "browser_download_url": "%s/test.exe"}, {"name": "SHA256SUMS", "size": 50, "browser_download_url": "%s/SHA256SUMS"}]}]`, mockGitea.URL, mockGitea.URL))) + } else if strings.Contains(r.URL.Path, "SHA256SUMS") { + w.Write([]byte(`b380cbb6489437721e1674e3b2736c699f5ffe8827e83e749b4f72417ea7e12c test.exe`)) + } else { + w.Write([]byte(`{"description": "Test Repo", "topics": ["test", "mock"], "licenses": ["MIT"], "private": false, "avatar_url": "https://example.com/logo.png"}`)) + } + })) + defer mockGitea.Close() + + giteaServer = mockGitea.URL + statsService := stats.NewService("test-hashes.json") + initialSoftware := config.LoadSoftware(configPath, giteaServer, "") + apiServer := api.NewServer("", initialSoftware, statsService) + + r := chi.NewRouter() + r.Use(security.SecurityMiddleware(statsService)) + r.Get("/api/software", apiServer.APISoftwareHandler) + r.Get("/api/stats", statsService.APIStatsHandler) + r.Get("/api/download", apiServer.DownloadProxyHandler) + r.Get("/api/avatar", apiServer.AvatarHandler) + r.Get("/api/rss", apiServer.RSSHandler) + + t.Run("API Software", func(t *testing.T) { + req := httptest.NewRequest("GET", "/api/software", nil) + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rr.Code) + } + + var sw []models.Software + json.Unmarshal(rr.Body.Bytes(), &sw) + if len(sw) == 0 || sw[0].Name != "software-station" { + t.Errorf("unexpected software list: %v", sw) + } + + if sw[0].Releases[0].Body != "Release notes for v1.0.0" { + t.Errorf("expected release body, got %s", sw[0].Releases[0].Body) + } + }) + + t.Run("RSS Feed", func(t *testing.T) { + req := httptest.NewRequest("GET", "/api/rss", nil) + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rr.Code) + } + + body := rr.Body.String() + if !strings.Contains(body, "") == false { + // Should be empty channel + } + }) + + t.Run("Avatar Proxy & Cache", func(t *testing.T) { + avatarData := []byte("fake-image-data") + avatarServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "image/png") + w.Write(avatarData) + })) + defer avatarServer.Close() + + hash := apiServer.RegisterURL(avatarServer.URL) + req := httptest.NewRequest("GET", fmt.Sprintf("/api/avatar?id=%s", hash), nil) + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rr.Code) + } + if rr.Header().Get("Content-Type") != "image/png" { + t.Errorf("expected image/png, got %s", rr.Header().Get("Content-Type")) + } + + // Verify it was cached + cachePath := filepath.Join(".cache/avatars", hash) + if _, err := os.Stat(cachePath); os.IsNotExist(err) { + t.Error("avatar was not cached to disk") + } + }) + + t.Run("API Stats", func(t *testing.T) { + req := httptest.NewRequest("GET", "/api/stats", nil) + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rr.Code) + } + }) + + t.Run("Download Proxy Throttling", func(t *testing.T) { + assetServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(make([]byte, 1024)) + })) + defer assetServer.Close() + + hash := apiServer.RegisterURL(assetServer.URL) + req := httptest.NewRequest("GET", fmt.Sprintf("/api/download?id=%s", hash), nil) + req.Header.Set("User-Agent", "aria2/1.35.0") + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rr.Code) + } + }) + + t.Run("Download Range Request", func(t *testing.T) { + content := "0123456789" + assetServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Range") == "bytes=2-5" { + w.Header().Set("Content-Range", "bytes 2-5/10") + w.WriteHeader(http.StatusPartialContent) + w.Write([]byte(content[2:6])) + } else { + w.Write([]byte(content)) + } + })) + defer assetServer.Close() + + hash := apiServer.RegisterURL(assetServer.URL) + req := httptest.NewRequest("GET", fmt.Sprintf("/api/download?id=%s", hash), nil) + req.Header.Set("Range", "bytes=2-5") + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + if rr.Code != http.StatusPartialContent { + t.Errorf("expected 206, got %d", rr.Code) + } + }) + + t.Run("Security - Path Traversal", func(t *testing.T) { + patterns := []string{"/.git/config", "/etc/passwd"} + for _, p := range patterns { + req := httptest.NewRequest("GET", p, nil) + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + if rr.Code != http.StatusForbidden { + t.Errorf("expected 403 for %s, got %d", p, rr.Code) + } + } + }) + + t.Run("Security - XSS in API", func(t *testing.T) { + malicious := []models.Software{{Name: ""}} + testStatsService := stats.NewService("test-hashes.json") + srv := api.NewServer("", malicious, testStatsService) + req := httptest.NewRequest("GET", "/api/software", nil) + rr := httptest.NewRecorder() + srv.APISoftwareHandler(rr, req) + if !strings.Contains(rr.Body.String(), "script") { + t.Error("XSS payload missing") + } + }) + + t.Run("Download Proxy - Speed Downloader", func(t *testing.T) { + assetServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(make([]byte, 100)) + })) + defer assetServer.Close() + hash := apiServer.RegisterURL(assetServer.URL) + req := httptest.NewRequest("GET", fmt.Sprintf("/api/download?id=%s", hash), nil) + req.Header.Set("User-Agent", "aria2/1.35.0") + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rr.Code) + } + }) + + t.Run("Static Files", func(t *testing.T) { + // Mock the filesystem behavior for the static handler + // We'll just test if the router handles unknown paths correctly + req := httptest.NewRequest("GET", "/unknown-path", nil) + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + // It should try to serve index.html if file not found, but since build might be empty in tests, it might 500 or 404 + if rr.Code == http.StatusOK || rr.Code == http.StatusNotFound || rr.Code == http.StatusInternalServerError { + t.Logf("Static handler returned %d", rr.Code) + } + }) + + t.Run("API Stats - Status unhealthy", func(t *testing.T) { + statsService.GlobalStats.Lock() + statsService.GlobalStats.TotalRequests = 100 + statsService.GlobalStats.BlockedRequests["bad"] = true + // Make it more than 50% blocked + for i := 0; i < 60; i++ { + statsService.GlobalStats.BlockedRequests[fmt.Sprintf("bad%d", i)] = true + } + statsService.GlobalStats.Unlock() + + req := httptest.NewRequest("GET", "/api/stats", nil) + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + var s map[string]interface{} + json.Unmarshal(rr.Body.Bytes(), &s) + if s["status"] != "unhealthy" { + t.Errorf("expected unhealthy status, got %v", s["status"]) + } + + // Reset + statsService.GlobalStats.Lock() + statsService.GlobalStats.BlockedRequests = make(map[string]bool) + statsService.GlobalStats.TotalRequests = 0 + statsService.GlobalStats.Unlock() + }) +} + +func TestOSDetection(t *testing.T) { + if gitea.DetectOS("test.exe") != models.OSWindows { + t.Error("expected windows") + } +} diff --git a/software.txt b/software.txt new file mode 100644 index 0000000..fc1989d --- /dev/null +++ b/software.txt @@ -0,0 +1 @@ +Quad4-Software/webnews