From 1d5d6aacb4d146334389c667d0935d0f0ff6ced1 Mon Sep 17 00:00:00 2001 From: Sudo-Ivan Date: Sat, 27 Dec 2025 02:57:25 -0600 Subject: [PATCH] 0.1.0 --- .dockerignore | 27 + .gitignore | 30 + LICENSE | 22 + Makefile | 46 + frontend/.gitignore | 23 + frontend/.npmrc | 1 + frontend/.prettierignore | 6 + frontend/.prettierrc | 8 + frontend/README.md | 38 + frontend/eslint.config.js | 59 + .../frontend/static/icons/gitea-dark.webp | Bin 0 -> 2768 bytes frontend/package.json | 39 + frontend/pnpm-lock.yaml | 3008 +++++++++++++++++ frontend/postcss.config.js | 6 + frontend/src/app.css | 83 + frontend/src/app.d.ts | 13 + frontend/src/app.html | 11 + frontend/src/lib/assets/favicon.svg | 1 + frontend/src/lib/components/SearchBar.svelte | 22 + .../src/lib/components/SoftwareCard.svelte | 369 ++ .../src/lib/components/icons/BrandIcon.svelte | 35 + frontend/src/lib/i18n/index.ts | 13 + frontend/src/lib/i18n/locales/de.json | 45 + frontend/src/lib/i18n/locales/en.json | 45 + frontend/src/lib/i18n/locales/it.json | 45 + frontend/src/lib/i18n/locales/ru.json | 45 + frontend/src/lib/index.ts | 1 + frontend/src/lib/types.ts | 26 + frontend/src/routes/+layout.svelte | 162 + frontend/src/routes/+layout.ts | 3 + frontend/src/routes/+page.svelte | 106 + frontend/src/routes/legal/[doc]/+page.svelte | 81 + frontend/static/.well-known/security.txt | 6 + frontend/static/icons/gitea-dark.webp | Bin 0 -> 2768 bytes frontend/static/icons/gitea.webp | Bin 0 -> 2774 bytes frontend/static/logo.png | Bin 0 -> 3770 bytes frontend/static/robots.txt | 3 + frontend/svelte.config.js | 24 + frontend/tailwind.config.js | 50 + frontend/tsconfig.json | 20 + frontend/vite.config.ts | 11 + go.mod | 17 + go.sum | 20 + internal/api/constants.go | 10 + internal/api/handlers.go | 425 +++ internal/cache/cache.go | 44 + internal/cache/cache_test.go | 43 + internal/config/config.go | 86 + internal/config/config_test.go | 62 + internal/config/constants.go | 6 + internal/config/updater.go | 30 + internal/gitea/client.go | 298 ++ internal/gitea/client_test.go | 118 + internal/gitea/constants.go | 9 + internal/models/constants.go | 12 + internal/models/models.go | 41 + internal/security/constants.go | 50 + internal/security/security.go | 220 ++ internal/security/security_test.go | 176 + internal/stats/constants.go | 5 + internal/stats/stats.go | 154 + internal/stats/stats_test.go | 54 + legal/disclaimer.txt | 6 + legal/privacy.txt | 10 + legal/terms.txt | 10 + main.go | 166 + main_test.go | 278 ++ software.txt | 1 + 68 files changed, 6884 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 frontend/.gitignore create mode 100644 frontend/.npmrc create mode 100644 frontend/.prettierignore create mode 100644 frontend/.prettierrc create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/frontend/static/icons/gitea-dark.webp create mode 100644 frontend/package.json create mode 100644 frontend/pnpm-lock.yaml create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/app.css create mode 100644 frontend/src/app.d.ts create mode 100644 frontend/src/app.html create mode 100644 frontend/src/lib/assets/favicon.svg create mode 100644 frontend/src/lib/components/SearchBar.svelte create mode 100644 frontend/src/lib/components/SoftwareCard.svelte create mode 100644 frontend/src/lib/components/icons/BrandIcon.svelte create mode 100644 frontend/src/lib/i18n/index.ts create mode 100644 frontend/src/lib/i18n/locales/de.json create mode 100644 frontend/src/lib/i18n/locales/en.json create mode 100644 frontend/src/lib/i18n/locales/it.json create mode 100644 frontend/src/lib/i18n/locales/ru.json create mode 100644 frontend/src/lib/index.ts create mode 100644 frontend/src/lib/types.ts create mode 100644 frontend/src/routes/+layout.svelte create mode 100644 frontend/src/routes/+layout.ts create mode 100644 frontend/src/routes/+page.svelte create mode 100644 frontend/src/routes/legal/[doc]/+page.svelte create mode 100644 frontend/static/.well-known/security.txt create mode 100644 frontend/static/icons/gitea-dark.webp create mode 100644 frontend/static/icons/gitea.webp create mode 100644 frontend/static/logo.png create mode 100644 frontend/static/robots.txt create mode 100644 frontend/svelte.config.js create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/api/constants.go create mode 100644 internal/api/handlers.go create mode 100644 internal/cache/cache.go create mode 100644 internal/cache/cache_test.go create mode 100644 internal/config/config.go create mode 100644 internal/config/config_test.go create mode 100644 internal/config/constants.go create mode 100644 internal/config/updater.go create mode 100644 internal/gitea/client.go create mode 100644 internal/gitea/client_test.go create mode 100644 internal/gitea/constants.go create mode 100644 internal/models/constants.go create mode 100644 internal/models/models.go create mode 100644 internal/security/constants.go create mode 100644 internal/security/security.go create mode 100644 internal/security/security_test.go create mode 100644 internal/stats/constants.go create mode 100644 internal/stats/stats.go create mode 100644 internal/stats/stats_test.go create mode 100644 legal/disclaimer.txt create mode 100644 legal/privacy.txt create mode 100644 legal/terms.txt create mode 100644 main.go create mode 100644 main_test.go create mode 100644 software.txt 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 0000000000000000000000000000000000000000..7b4ebb55985ba095cbd47b06c6f85b6ef5b29e33 GIT binary patch literal 2768 zcmV;>3NQ6iNk&G<3IG6CMM6+kP&iDx3IG5v|G|F{9}p1=(l%hKY0v8H2Krz*+WxnZ zqd&kJ;(!BBGP^PeU=2bbLXaC5z{*P<0uh3|)O(iYx)8t+7y=IK?HC-;Q^X%tP4Z{H z-~ZQp?1=tLlw_+4qamA9Mr4u4xqyEV+^1Xrx%Ho0|GBAXF1Oq5+jsr)U7Sx+w|)4W z&wn$Wwr}6fQswUDPfHPhnFZ-j?E^?A(kPVblX*&X*p zziiI#c&B$vcY4P?(L2rToxOW{&&($|-7|TonZ48cMw)Z`_V=C`c;3&h2ZJX@)(V~v z!5I_x@9Z?t6EnllgEROiFT(js*bjS#r9*m79WpRVF!^C*)FTr~>5@p-oZ{dI=9*%3 zLqlJCgt5KcC42E_fWZs2?bROX!XxrCkWU8jSCB_uplh1>^9gDDxl1-J`6R>6A6@Xtjw*|z^>Xe=4X@fMl)Y3OjXz4C)Q>3JampYk_#Sfv|nAObQN8zIzgEZXQM-l*H^>yQk(Ed{C%x|Lc*HU;d$%Jv_Y+ z7B?AtALKU(E1sPXvYU+d|06*|Yif#P7Z$xG3!Vl-MG}v7e+DKaDFh2Y3){)VCtzrE zFKrfH1vgBtOS*#6slnS$cg-hg9%(inVZz(aIvfR!YTAUDC08-yam_IS%6E%1y?XkL-%Lx1{ZdQ&`aNEEG--@kY9e7d0 zJ2ZUfTeAJL4rr-(a+X%hf@KrXvf_hVfew7R0D(aIEvg>WlCg9_AZTiE;Ol|}uyYrD zaOuE-XCv^3C3@++5HwAO-TBoda%mCccAzt2yRDM=(@Wr`OD5==3LAJz30)?*0cMTe zc@<>u5(2%^NzApw4%?iZ-U`_RXqk6`1|ISiBPGa;8<97a$iUMH{b7T2zBMs56wcS; zHa+Cj1wL3)=0=Vipl{doFNzB!)YMco8}7{G$iIZCjM_RQl< zIvBZha4ng@gFN$~Bu9f7! zESQCJAZcg$PpfM+VrbMHHN^Hh#QOG6cdmii4$}-3#P&7}qx9|Wg+^0r(wa&Zi0!S9 z0P6RXW}T>BYI9( zMdYSOJPjUcz@3m{Y-bU~(_mDF9YCSTO^h#iMvscU()R5hrW^Vbz6~43_S9He zpI}|HklWs%vV9Q^DzhN2=L*}?+P<0f2nKc*#M5w6+P*F`_=9U9pN3Io`@(bux4rRn z7_gfFi0y2od;+(<>D0r-QMlF3Tw7r~wiesoUI#)TgXXLg#ZkD|*}kc4*n!oFS6ze7 z_T_?sA6ViufDvK4M$vE0lM(dvD3}1NZlU=i*$H?=+g?=KzU7~44I^2rM(G@y^bcz) z%662ttNN3{h0yl)TH96qDZ;ZzZLinWT$#Q_7t~2@Z_wGU?N7)1p49e6o$adAUr@du zPe+k-6ENv)SD(Iv#@$+Kdr@n<`kSD22fqx3+0Ft%_~IF&*#@o`zk9~mj?Q)=srd;o zZ1&7se8Sa9X}h5G)>a1Jw;j1=K-o?gaOt(ne+uhOXRN7#2D6<_z^1!bl76FC2}`P@ zVYXAF(6nGl#;6OfdKtuaDm1;S;a+>2ebEKPc2;QisEHG~E}2!fi$difFNth|>(f?6 zkpP`UT5d?}l(vUXGe&mYlA_ny9-N4gJ?ANpbhZcQfRUbKOvCnwcYsLGHR>S=+f!Qj zOIo6>?Fn87&r6guM%9qH5SrE3fz|+hS*)#M?+ry^_ksZ?*k5mK$CMg#h z-yf5)J?&d8C4*xHu$?s)Z7*IZ;(oNZJ$6n(WTXf6oWQDw8`CP0t@~Q7?O7BBk$s}J zqaH>V?0Lyc5Zf_f(g=4W$JmZdQ4o18&c8rxdO91186#gj%+A{>A={zG5ypE`+w{3GDAOW3u7%_nabmn2iIAfi>SC(Z~6l5cTI`gn93L}^^4=Z*v zRwnbZlrtH!35scyC4UvfCaBSsWyS=RR$G=?QApV<%d&eADSZl2S7fC~8Ip;*+*m@& zL0OjFg8+e*W%nRJU}YITh?AZ2Ny`Uu(kr8$<%0-8MLoO7gAmN3)AB)>d@?Jaw9JT* z5A^lRO&QKmFM*q=e9|~0;!pQpyPB0x+Gb23)wYC~0HrZw0u;s!36LAJN1=SuLq_eB z&NEi#jP_5O)2EQrdB)6~#(T~XCTrJEfWbbe^Ne`{vXnn`++M}Xyy7(*-qKP)wSwe)- zs#NEfMN%};=-T&X71H7p1D2=wwZ4mx8h^IQomghP=R21dLP${1_npf!Ny0yX{5+&d W^V1fUo)7NRt^eHm&#nL5d;|dUSz{Cc literal 0 HcmV?d00001 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 @@ + + +
+ + +
+
+
+
+ +
+

+ {#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 0000000000000000000000000000000000000000..7b4ebb55985ba095cbd47b06c6f85b6ef5b29e33 GIT binary patch literal 2768 zcmV;>3NQ6iNk&G<3IG6CMM6+kP&iDx3IG5v|G|F{9}p1=(l%hKY0v8H2Krz*+WxnZ zqd&kJ;(!BBGP^PeU=2bbLXaC5z{*P<0uh3|)O(iYx)8t+7y=IK?HC-;Q^X%tP4Z{H z-~ZQp?1=tLlw_+4qamA9Mr4u4xqyEV+^1Xrx%Ho0|GBAXF1Oq5+jsr)U7Sx+w|)4W z&wn$Wwr}6fQswUDPfHPhnFZ-j?E^?A(kPVblX*&X*p zziiI#c&B$vcY4P?(L2rToxOW{&&($|-7|TonZ48cMw)Z`_V=C`c;3&h2ZJX@)(V~v z!5I_x@9Z?t6EnllgEROiFT(js*bjS#r9*m79WpRVF!^C*)FTr~>5@p-oZ{dI=9*%3 zLqlJCgt5KcC42E_fWZs2?bROX!XxrCkWU8jSCB_uplh1>^9gDDxl1-J`6R>6A6@Xtjw*|z^>Xe=4X@fMl)Y3OjXz4C)Q>3JampYk_#Sfv|nAObQN8zIzgEZXQM-l*H^>yQk(Ed{C%x|Lc*HU;d$%Jv_Y+ z7B?AtALKU(E1sPXvYU+d|06*|Yif#P7Z$xG3!Vl-MG}v7e+DKaDFh2Y3){)VCtzrE zFKrfH1vgBtOS*#6slnS$cg-hg9%(inVZz(aIvfR!YTAUDC08-yam_IS%6E%1y?XkL-%Lx1{ZdQ&`aNEEG--@kY9e7d0 zJ2ZUfTeAJL4rr-(a+X%hf@KrXvf_hVfew7R0D(aIEvg>WlCg9_AZTiE;Ol|}uyYrD zaOuE-XCv^3C3@++5HwAO-TBoda%mCccAzt2yRDM=(@Wr`OD5==3LAJz30)?*0cMTe zc@<>u5(2%^NzApw4%?iZ-U`_RXqk6`1|ISiBPGa;8<97a$iUMH{b7T2zBMs56wcS; zHa+Cj1wL3)=0=Vipl{doFNzB!)YMco8}7{G$iIZCjM_RQl< zIvBZha4ng@gFN$~Bu9f7! zESQCJAZcg$PpfM+VrbMHHN^Hh#QOG6cdmii4$}-3#P&7}qx9|Wg+^0r(wa&Zi0!S9 z0P6RXW}T>BYI9( zMdYSOJPjUcz@3m{Y-bU~(_mDF9YCSTO^h#iMvscU()R5hrW^Vbz6~43_S9He zpI}|HklWs%vV9Q^DzhN2=L*}?+P<0f2nKc*#M5w6+P*F`_=9U9pN3Io`@(bux4rRn z7_gfFi0y2od;+(<>D0r-QMlF3Tw7r~wiesoUI#)TgXXLg#ZkD|*}kc4*n!oFS6ze7 z_T_?sA6ViufDvK4M$vE0lM(dvD3}1NZlU=i*$H?=+g?=KzU7~44I^2rM(G@y^bcz) z%662ttNN3{h0yl)TH96qDZ;ZzZLinWT$#Q_7t~2@Z_wGU?N7)1p49e6o$adAUr@du zPe+k-6ENv)SD(Iv#@$+Kdr@n<`kSD22fqx3+0Ft%_~IF&*#@o`zk9~mj?Q)=srd;o zZ1&7se8Sa9X}h5G)>a1Jw;j1=K-o?gaOt(ne+uhOXRN7#2D6<_z^1!bl76FC2}`P@ zVYXAF(6nGl#;6OfdKtuaDm1;S;a+>2ebEKPc2;QisEHG~E}2!fi$difFNth|>(f?6 zkpP`UT5d?}l(vUXGe&mYlA_ny9-N4gJ?ANpbhZcQfRUbKOvCnwcYsLGHR>S=+f!Qj zOIo6>?Fn87&r6guM%9qH5SrE3fz|+hS*)#M?+ry^_ksZ?*k5mK$CMg#h z-yf5)J?&d8C4*xHu$?s)Z7*IZ;(oNZJ$6n(WTXf6oWQDw8`CP0t@~Q7?O7BBk$s}J zqaH>V?0Lyc5Zf_f(g=4W$JmZdQ4o18&c8rxdO91186#gj%+A{>A={zG5ypE`+w{3GDAOW3u7%_nabmn2iIAfi>SC(Z~6l5cTI`gn93L}^^4=Z*v zRwnbZlrtH!35scyC4UvfCaBSsWyS=RR$G=?QApV<%d&eADSZl2S7fC~8Ip;*+*m@& zL0OjFg8+e*W%nRJU}YITh?AZ2Ny`Uu(kr8$<%0-8MLoO7gAmN3)AB)>d@?Jaw9JT* z5A^lRO&QKmFM*q=e9|~0;!pQpyPB0x+Gb23)wYC~0HrZw0u;s!36LAJN1=SuLq_eB z&NEi#jP_5O)2EQrdB)6~#(T~XCTrJEfWbbe^Ne`{vXnn`++M}Xyy7(*-qKP)wSwe)- zs#NEfMN%};=-T&X71H7p1D2=wwZ4mx8h^IQomghP=R21dLP${1_npf!Ny0yX{5+&d W^V1fUo)7NRt^eHm&#nL5d;|dUSz{Cc literal 0 HcmV?d00001 diff --git a/frontend/static/icons/gitea.webp b/frontend/static/icons/gitea.webp new file mode 100644 index 0000000000000000000000000000000000000000..4e30d4e5d231d21d323380adb6eaa4a399f7d242 GIT binary patch literal 2774 zcmV;{3MutcNk&G_3IG6CMM6+kP&iD%3IG5v|G|F{A29#(KmQ8SHejl0&+6<3xNr=X zqwRkiIr;;vAr3h3B(p1n0M;M`A_Tc{0j#{#ArK+ROTA}Vt_uMSfg#|q-j2ZmJw^Oc z)g*uB`~82t$ByW~L`kx$iW!n@c}0Re_5l7taG!4d=hlC2{pY5lx!i8IZ{PLHcX2*R z-S**gKL5^i+P-}^OO?BqKP^T4Wfr7Ay`S75c zr;h%?!8=WK$(#}Nvrlp=w^J6x_3V^_xb{j(T)Tz1_R8vvy<4g?M)yRw)aNnBJH21l zXLsBa{jxc` zm}`pB4Gn$m5ytj%m+Zx#0R}J7wpV+k3y;XpKt377UqK#ufv#!hlLNU2r|G>w`g`q^ zDE*2}pgHN0x4Q1()BH2x@kQa_?>$0>uH z;E$XoI$^v)$b3>!1eVDO2g3HfGATGX`R+-uxp^3UP!h9i?Vg%v@Ij%%{XdVK{No>L z*~8QOU~!YN_d$Mxu;SVIAiK$E|34BmG%n~UDnmX9U&JH{xVm4z?k8yyQM`1l<7mYu z3G$Okk|YzX>L=;X#}+VPO;Mxa+pD?mrC+4R0Yn)n$?()VM%W!nE<8bQmq=N{sRFg` zM?-E`Xi19!)pLwIL$jjT2zUglx$#L+siOxynGT&53-YAWhDpO7LvDOhUXbQ$MP~e^ zWI>YIEId1Ip8gL`PMUkp(?ZyAK2=6Mqct_fu?vgdk_Ar#p(2ULx<3PxkraZ3pM~w@ z;S(^lxtBHzuYwyU*Cky+>D1tDr@Q78G>Dlz;r>6KR9vcoUr#=x?#;b*H z=uh~8^Jfe>!x|oV>4v`ICr%S#OqB5%1m1${<}AP^`)WZ0cwO(K^TF>VJsBevTri9fvrUb)GHDzw(xB>chP5(o2frOfxie|%|c^vtd(0nIA#RukXfVNfg-IHIP z)*Q!*nB1B|C)An8Y1yC1hRy-daQj(P=mB`<@qTuG+)%h79I>$jt%K zt{w8oladXbmaPyeEy#Ar&g%J$kb&FG3Tbdhp3GJ!!&H1t_Kik>wnGo%9Mf^N#@#B& zuwjopnVGTxO&pcGEdkTEV{M1&=mKLLD^=rMWP00Nv$n(40%M$|Zg9M$4?Hz%J7g~~ z3G z$pW#x^$|e*es?EgRy_qhhiHZ)nwmrUCt_ATIoE|Ni0xFE4IXaQlW~-oT@Pl116uX4 zbLc=5)h(wUAh1=>CWwb&qiZ?t2AYiX8Jb9%a;sqxOmB4jJd-Yh!w%4M)C|G0;pdq+ z3igPe(^V0<=@CzZM;dS^q!`;-1o1Q&m0<@^D00&yo(2bH*Z~xZ+w=r>8bI1|9Uzy$ zO^{nQHiXfmVz0D)yNBt9{)BJC zhOs?0R@Nt2*DU0=H>hl1M1#sKi0iq+_O!NdW<7#|odxkUoRqe&%MAYDTF9qiRN1~T zUBPW{JRJt?CIDhP8!4Z_ZErgDFmV)abu-si*p98mwzt=T5Xhi8>qK!B?sc|rDjRlS zb>damptF6sVBiOq_zYk~*sf9ZTk~WDJv|C0z^YqlzDRZg9?`ZJm9}sBQ>|enYt<;7 zLzDhtO-0#`(sorp8C(c$Z?Cmo)lU(gMQVG!uI9@0ExMpiYI}puc5VN3yzfbEZ`9eY zI{gLZ`|)%XNjCwL&UW?bJ80akrM4HfwyVDhT6gfvP?+s35QHzDA)0OAdhxqwjP2-b z7m}Kv0K;a_yu~M6os_lH404&mSl{&;HsBFY^Orgs~YaLx7in6Kx}7)W{;XUk?WFKWxFU;4)T)7 zCb&LrRTK%(Nu=e5#7=2@=rm(w$1N#(o$bMi7};~4@k9Y@&^jxDJ zlCV9cg}I}1F`43RxUue3dUiV)c`jqylD zFkq5$q4E7O8Qaso#ZodjRsh>sW6}2Fg(B`pd)s5@6huaPP|pdhdblyI64|=1)!Lp# zQ4rZDYCGy-bitmNyacfw6DEytH*$>a*c1hk*W&yO#HOdSVVE)U#l!5pof5JgY8+v_ zC$&uvJ*QM6BBoT_QLZ{{(hL&NnTHV*NI+*EMvODo$$Mp4W<^0Z5~wo|tD-Q1 zIrFe$H)CZoFH1R-A)BC>Mp^P#L2QB=U0G&KP-(SgnH7bUy|OI32a(dJ5OqaXij*Ol zsLPEdq>Re4>>dONtSq|+0Rk(_@IjpHluuedh?8C!^(-Gm2rBB?MIMA;7M+$4!sL@# z`J`n=gnXc{UvA29hI$FyMCFsl84*9-d+lmgK53gVfmGWPVgi)Lj0sQ}GbBK6%pQgE zNe>ydPdd+7nKRlyX-=O)PUjgja~khCLzt{xKLG~&oX#`m3D72x)86^)bGmlEDhYb) zoX&6LNf4rK(06{LPK4g}j`JINB7|sp$N7yq8G6e*&Tr(&5Tf-Rw`-4z!s+6(NQfrd zTxSUpMypbtUlvKxM5AlpmsLoMPYhU|;@A2vLTdckCU;_)@t*HoUI-yULEm>S%OnZ^ c0P^#YCe2Sp!>tbMp}Z0K#5Rt^fc4 literal 0 HcmV?d00001 diff --git a/frontend/static/logo.png b/frontend/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..145213310acc8edaee7ff4c574a0fc7f7345d4bb GIT binary patch literal 3770 zcmd^?_d6Tv`^Qz)sJ4{aBmGjXBeqf+yH=F^va=?FQ3&(x&>^X2Wi&Bpp37_j}FS zKN;e0JYbi+ALnw7g@M)JzO5#GJ3Yt$($ieV2Rlh_qeTsEw3(j78-e^DmlxIbc9PZ# zgdo8ue~V9yaU#04(RT`K#JCX{?1anXrvebM9rjKrQ#v~MVkZbQBuy&9-$(>m1@qgVK?Jm1TtoWH3m}kvioCN!kp6tc} zJg!a?cfKun77%Qlnz~^xEx%9of*BLyAPLcO`0$MJ=QYFgre2?(ej=ba8Rf56@(q_%-4_x4j;` z_rYL!cE@{SBS^9G(&z>LR4mhrBQj^w+3)MtKeKvRT@CWddGqNZKAOak5D6WsFLNdr zbra-6w|i3kuxc#*OdI(__H)DvtNan2p~ANNf+~yRK+}eT4{`xTUMmu~O*iU7^J%aIS=o0eH@n2}hL&vuMXWKTpJ4!E?Tdr{Wc~1OK;GnR)H4tG z$==G%Y)MH;5nqIAHXIDK!BG_4GqXcD4#q3F#`73BZQBDsW<}zMVDOJChf1_BD>(Tu zt_3$gqR5zi^$*y!T%?G1?4MLsZSh-w3*CG_CxkXDN0V>gCK@shxICVinD8oxq&)Ns zGSLGS6*fBpi<8;^R?%tvO$H-`Q4LH(p2`IdL<%I;}bQq}~Pej&2+Z2^H3{ zRqcj-tEdQ?tafB-JCy(ef!rH%yhs%X6NoEqL0nxwn3QGOWiNf@0%EcA6@M${&g7{` zs1J=4>p$@g3)(G$ev}kcyIWOhTaJ}e5iFbBFfRQs7OUY$9dIY77O1JLa5@k(?IJ6C z6sRmH9VBTMA|_7Qs`g|n4-Nsy@}^em38dCF#JY_Cn57!8+fDW+0qSo z7F-_x)944q>GYL-{OPmMH3jlYdzoB#lNDhrG-@X90nBL^?npa47$;81bN^J{KHcoL z2Sofz18uWQ&D5eHFsY+to6@d`2tz=jz|Fd(Gp;nh0Kr~mKSuh?$LUs!?eZuwwx;N4 z{J!C;#58ttwyU*dDfnrYVxW$cWAk(DtE~a}(GE$*?r{*sy(e-T?LBF#s;Gz@tw@3R z`*gK(c*A{&GtGb$2KcAwP~Ylh0}72PiNQcsVO*9fN2*>4_s^3k)E1=MSct_`RW6n? z*sFv1Bo)FEUwhv4G9e)WQ!+h0je3Hc3pDcdU2jO#7LuNfmE$hQ?8-j<8WHUaw+Ag5 z{#e4WFupFEAH28O@U_pw+zK_Xj)K_JI7G8v)kjy#x%ZqO`o;fx=Pac9(V6mf;Y)Ss!QjeAK0#YzSk46i!ULI@`h(VGHu--2I(Twgw+Dc z9f`{toJ*la@5;Ex{x)Ga!#V_a4Uom%E6rcjnQeI}CXw|ekFsD%ZwIR7cvJbJG;)XX z&%Rnh{sr)!65M;!V$OG4*Tw)~NAntjBgCqk7do;LNuSAvB>mysih zaM(m64o8i;Xnck7%IKT@28KOTnngapVZ9=(RJbBgT^jvwUce|9Yllr0F_%`n z4;zh_kH5C&YfNr!I$kn7hWCQ16k1;1!U~r zA(FGL$K>GLrn1~^u+`w#&Q7|6fP6~t($jqDw*!#9&$g<~ z@gw17)6JP(&IkJF7&~=tpyXd7%-VZgnO>vlPBO}u;{@i|8WrAyTyr*jj^{7MuAlG| zo{khXKnaxjE5h~b*ZXWZ(rpc`h_kbx_krgYA)t^UqYCHnm8+yyb9-FDsudfDao#I8 zL7t=nE1HxYx8UnAC?O2IY8v{kmVir*Kmcv3Gwi~yvfC^!|1d64&8eGr9b+7e>MFRH zBt~lvZNGFKv`b?<|NMn<;(H6Jt)bpL@K61Q{{EZOq|xu*)UhQDcwWT)G{nXxd#^to zpmDi{BN=8jf0E4Nj~J>nc;F4pk)szdH=uk6P;~d{{6@{H! z!i*?!!o{795Gp${Vi?0(!YQu*UE 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