diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..398ca13 Binary files /dev/null and b/.DS_Store differ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1b5373a --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +DB_URL="postgresql://user:password@db:5432/esports" + +# Drizzle +# Replace with your DB credentials! +DATABASE_URL="postgres://user:password@host:port/db-name" + +ORIGIN="" + +# Better Auth +# For production use 32 characters and generated with high entropy +# https://www.better-auth.com/docs/installation +BETTER_AUTH_SECRET="" +# GitHub OAuth +# https://www.better-auth.com/docs/authentication/github +GITHUB_CLIENT_ID="" +GITHUB_CLIENT_SECRET="" diff --git a/.gitignore b/.gitignore index c2658d7..41f1bdc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,25 @@ -node_modules/ +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-* +# Playwright +test-results diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..0243576 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb + +# Miscellaneous +/static/ +/drizzle/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..819fa57 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,16 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ], + "tailwindStylesheet": "./src/routes/layout.css" +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bc31e15 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "*.css": "tailwindcss" + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..18c4bd3 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# 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 +npx sv create my-app +``` + +To recreate this project with the same configuration: + +```sh +# recreate this project +bun x sv create --template minimal --types ts --add prettier eslint vitest="usages:unit,component" playwright tailwindcss="plugins:typography" sveltekit-adapter="adapter:node" devtools-json drizzle="database:postgresql+postgresql:postgres.js+docker:no" better-auth="demo:password,github" --install bun . +``` + +## 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/bun.lock b/bun.lock index 8ebdea4..17c43ca 100644 --- a/bun.lock +++ b/bun.lock @@ -4,23 +4,847 @@ "workspaces": { "": { "name": "esports", - "devDependencies": { - "@types/bun": "latest", + "dependencies": { + "bancho.js": "^0.12.3", + "postgres": "^3.4.8", }, - "peerDependencies": { - "typescript": "^5", + "devDependencies": { + "@eslint/compat": "^2.0.2", + "@eslint/js": "^9.39.2", + "@playwright/test": "^1.58.1", + "@sveltejs/adapter-node": "^5.5.2", + "@sveltejs/kit": "^2.50.2", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.1.18", + "@types/node": "^22", + "@vitest/browser-playwright": "^4.0.18", + "better-auth": "^1.4.18", + "drizzle-kit": "^0.31.8", + "drizzle-orm": "^0.45.1", + "eslint": "^9.39.2", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.14.0", + "globals": "^17.3.0", + "playwright": "^1.58.1", + "prettier": "^3.8.1", + "prettier-plugin-svelte": "^3.4.1", + "prettier-plugin-tailwindcss": "^0.7.2", + "svelte": "^5.51.0", + "svelte-check": "^4.3.6", + "tailwindcss": "^4.1.18", + "typescript": "^5.9.3", + "typescript-eslint": "^8.54.0", + "vite": "^7.3.1", + "vite-plugin-devtools-json": "^1.0.0", + "vitest": "^4.0.18", + "vitest-browser-svelte": "^2.0.2", }, }, }, "packages": { - "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], + "@better-auth/core": ["@better-auth/core@1.4.19", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-uADLHG1jc5BnEJi7f6ijUN5DmPPRSj++7m/G19z3UqA3MVCo4Y4t1MMa4IIxLCqGDFv22drdfxescgW+HnIowA=="], - "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.4.19", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.19" } }, "sha512-ApGNS7olCTtDpKF8Ow3Z+jvFAirOj7c4RyFUpu8axklh3mH57ndpfUAUjhgA8UVoaaH/mnm/Tl884BlqiewLyw=="], - "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], + + "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/compat": ["@eslint/compat@2.0.2", "", { "dependencies": { "@eslint/core": "^1.1.0" }, "peerDependencies": { "eslint": "^8.40 || 9 || 10" }, "optionalPeers": ["eslint"] }, "sha512-pR1DoD0h3HfF675QZx0xsyrsU8q70Z/plx7880NOhS02NuWLgBCOMDL787nUeQ7EWLkxv3bPQJaarjcPQb2Dwg=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@1.1.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.4", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" } }, "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ=="], + + "@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], + + "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + + "@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.0", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ=="], + + "@rollup/plugin-json": ["@rollup/plugin-json@6.1.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA=="], + + "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="], + + "@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.5.4", "", { "dependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "rollup": "^4.59.0" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ=="], + + "@sveltejs/kit": ["@sveltejs/kit@2.53.3", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.3", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "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 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-tshOeBUid2v5LAblUpatIdFm5Cyykbw2EiKWOunAAX0A/oJaR7DOdC9wLR5Qqh9zUf3QUISA2m9A3suBdQSYQg=="], + + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="], + + "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="], + + "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="], + + "@testing-library/svelte-core": ["@testing-library/svelte-core@1.0.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0" } }, "sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@22.19.13", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw=="], + + "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="], + + "@vitest/browser": ["@vitest/browser@4.0.18", "", { "dependencies": { "@vitest/mocker": "4.0.18", "@vitest/utils": "4.0.18", "magic-string": "^0.30.21", "pixelmatch": "7.1.0", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.0.3", "ws": "^8.18.3" }, "peerDependencies": { "vitest": "4.0.18" } }, "sha512-gVQqh7paBz3gC+ZdcCmNSWJMk70IUjDeVqi+5m5vYpEHsIwRgw3Y545jljtajhkekIpIp5Gg8oK7bctgY0E2Ng=="], + + "@vitest/browser-playwright": ["@vitest/browser-playwright@4.0.18", "", { "dependencies": { "@vitest/browser": "4.0.18", "@vitest/mocker": "4.0.18", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "playwright": "*", "vitest": "4.0.18" } }, "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g=="], + + "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], + + "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="], + + "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="], + + "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], + + "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "bancho.js": ["bancho.js@0.12.3", "", { "dependencies": { "nodesu": "^0.7.2", "rate-limiter-flexible": "^2.3.12" }, "peerDependencies": { "weak-value-map": "^1.0.1" }, "optionalPeers": ["weak-value-map"] }, "sha512-0y4AJ7ehY2i4k3eTffXY35DqxXTbH/902BYTDGP03gstrWq33Ad2HU2PpyOSIlhWVRMJrBA/Ntx6UBvWeKdQXg=="], + + "better-auth": ["better-auth@1.4.19", "", { "dependencies": { "@better-auth/core": "1.4.19", "@better-auth/telemetry": "1.4.19", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-3RlZJcA0+NH25wYD85vpIGwW9oSTuEmLIaGbT8zg41w/Pa2hVWHKedjoUHHJtnzkBXzDb+CShkLnSw7IThDdqQ=="], + + "better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "devalue": ["devalue@5.6.3", "", {}, "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg=="], + + "drizzle-kit": ["drizzle-kit@0.31.9", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg=="], + + "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], + + "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "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.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg=="], + + "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], + + "eslint-plugin-svelte": ["eslint-plugin-svelte@3.15.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", "globals": "^16.0.0", "known-css-properties": "^0.37.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", "svelte-eslint-parser": "^1.4.0" }, "peerDependencies": { "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-QKB7zqfuB8aChOfBTComgDptMf2yxiJx7FE04nneCmtQzgTHvY8UJkuh8J2Rz7KB9FFV9aTHX6r7rdYGvG8T9Q=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrap": ["esrap@2.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@17.3.0", "", {}, "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], + + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "known-css-properties": ["known-css-properties@0.37.0", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="], + + "kysely": ["kysely@0.28.11", "", {}, "sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + + "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + + "limiter": ["limiter@1.1.5", "", {}, "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="], + + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "nanostores": ["nanostores@1.1.1", "", {}, "sha512-EYJqS25r2iBeTtGQCHidXl1VfZ1jXM7Q04zXJOrMlxVVmD0ptxJaNux92n1mJ7c5lN3zTq12MhH/8x59nP+qmg=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "nodesu": ["nodesu@0.7.3", "", { "dependencies": { "limiter": "^1.1.0" } }, "sha512-RTHkaRyt+alXkzp8TDH1r4dtkp38jeqFyGgFBCT6uSqcGIOUdh7EqghwN1PrcGVqTjPPjWUCnqoNay+JOMw3bg=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pixelmatch": ["pixelmatch@7.1.0", "", { "dependencies": { "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng=="], + + "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], + + "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], + + "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="], + + "postcss-safe-parser": ["postcss-safe-parser@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="], + + "postcss-scss": ["postcss-scss@4.0.9", "", { "peerDependencies": { "postcss": "^8.4.29" } }, "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], + + "postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + + "prettier-plugin-svelte": ["prettier-plugin-svelte@3.5.0", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-2lLO/7EupnjO/95t+XZesXs8Bf3nYLIDfCo270h5QWbj/vjLqmrQ1LiRk9LPggxSDsnVYfehamZNf+rgQYApZg=="], + + "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.7.2", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "rate-limiter-flexible": ["rate-limiter-flexible@2.4.2", "", {}, "sha512-rMATGGOdO1suFyf/mI5LYhts71g1sbdhmd6YvdiXO2gJnd42Tt6QS4JUKJKSWVVkMtBacm6l40FR7Trjo6Iruw=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + + "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], + + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "svelte": ["svelte@5.53.5", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-YkqERnF05g8KLdDZwZrF8/i1eSbj6Eoat8Jjr2IfruZz9StLuBqo8sfCSzjosNKd+ZrQ8DkKZDjpO5y3ht1Pow=="], + + "svelte-check": ["svelte-check@4.4.4", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-F1pGqXc710Oi/wTI4d/x7d6lgPwwfx1U6w3Q35n4xsC2e8C/yN2sM1+mWxjlMcpAfWucjlq4vPi+P4FZ8a14sQ=="], + + "svelte-eslint-parser": ["svelte-eslint-parser@1.5.1", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0", "semver": "^7.7.2" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-UbY7DYoDg+x4AKLUcX5xWuEWylgmm8ZD2Z89YT/AK6Wm/ckeMTnOMwr6AVC99znXbRC26xzWEPhSgmB62E07Gg=="], + + "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], + + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "vite-plugin-devtools-json": ["vite-plugin-devtools-json@1.0.0", "", { "dependencies": { "uuid": "^11.1.0" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-MobvwqX76Vqt/O4AbnNMNWoXWGrKUqZbphCUle/J2KXH82yKQiunOeKnz/nqEPosPsoWWPP9FtNuPBSYpiiwkw=="], + + "vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="], + + "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], + + "vitest-browser-svelte": ["vitest-browser-svelte@2.0.2", "", { "dependencies": { "@testing-library/svelte-core": "^1.0.0" }, "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0", "vitest": "^4.0.0" } }, "sha512-OLJVYoIYflwToFIy3s41pZ9mVp6dwXfYd8IIsWoc57g8DyN3SxsNJ5GB1xWFPxLFlKM+1MPExjPxLaqdELrfRQ=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + + "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/config-helpers/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "better-call/set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + + "eslint/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "eslint-plugin-svelte/globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], + + "rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "svelte-eslint-parser/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "vite/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + + "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], } } diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..000bccf --- /dev/null +++ b/compose.yml @@ -0,0 +1,19 @@ +services: + postgres: + image: postgres + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: vash_esports + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + redis: + image: redis + ports: + - "6379:6379" + +volumes: + postgres_data: diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..d57e081 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'drizzle-kit'; + +if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set'); + +export default defineConfig({ + schema: './src/lib/server/db/schema.ts', + dialect: 'postgresql', + dbCredentials: { url: process.env.DATABASE_URL }, + verbose: true, + strict: true +}); diff --git a/e2e/demo.test.ts b/e2e/demo.test.ts new file mode 100644 index 0000000..9985ce1 --- /dev/null +++ b/e2e/demo.test.ts @@ -0,0 +1,6 @@ +import { expect, test } from '@playwright/test'; + +test('home page has expected h1', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('h1')).toBeVisible(); +}); diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..9690379 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,39 @@ +import prettier from 'eslint-config-prettier'; +import path from 'node:path'; +import { includeIgnoreFile } from '@eslint/compat'; +import js from '@eslint/js'; +import svelte from 'eslint-plugin-svelte'; +import { defineConfig } from 'eslint/config'; +import globals from 'globals'; +import ts from 'typescript-eslint'; +import svelteConfig from './svelte.config.js'; + +const gitignorePath = path.resolve(import.meta.dirname, '.gitignore'); + +export default defineConfig( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs.recommended, + prettier, + ...svelte.configs.prettier, + { + languageOptions: { globals: { ...globals.browser, ...globals.node } }, + rules: { + // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. + // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors + 'no-undef': 'off' + } + }, + { + files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], + languageOptions: { + parserOptions: { + projectService: true, + extraFileExtensions: ['.svelte'], + parser: ts.parser, + svelteConfig + } + } + } +); diff --git a/index.ts b/index.ts deleted file mode 100644 index f67b2c6..0000000 --- a/index.ts +++ /dev/null @@ -1 +0,0 @@ -console.log("Hello via Bun!"); \ No newline at end of file diff --git a/package.json b/package.json index a1abd3a..e08ff9e 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,60 @@ { - "name": "esports", - "module": "index.ts", - "type": "module", - "private": true, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5" - } + "name": "esports", + "private": true, + "version": "0.0.1", + "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", + "lint": "prettier --check . && eslint .", + "format": "prettier --write .", + "test:unit": "vitest", + "test": "npm run test:unit -- --run && npm run test:e2e", + "test:e2e": "playwright test", + "db:push": "drizzle-kit push", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio", + "auth:schema": "npx @better-auth/cli generate --config src/lib/server/auth.ts --output src/lib/server/db/auth.schema.ts --yes" + }, + "devDependencies": { + "@eslint/compat": "^2.0.2", + "@eslint/js": "^9.39.2", + "@playwright/test": "^1.58.1", + "@sveltejs/adapter-node": "^5.5.2", + "@sveltejs/kit": "^2.50.2", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.1.18", + "@types/node": "^22", + "@vitest/browser-playwright": "^4.0.18", + "better-auth": "^1.4.18", + "drizzle-kit": "^0.31.8", + "drizzle-orm": "^0.45.1", + "eslint": "^9.39.2", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.14.0", + "globals": "^17.3.0", + "playwright": "^1.58.1", + "prettier": "^3.8.1", + "prettier-plugin-svelte": "^3.4.1", + "prettier-plugin-tailwindcss": "^0.7.2", + "svelte": "^5.51.0", + "svelte-check": "^4.3.6", + "tailwindcss": "^4.1.18", + "typescript": "^5.9.3", + "typescript-eslint": "^8.54.0", + "vite": "^7.3.1", + "vite-plugin-devtools-json": "^1.0.0", + "vitest": "^4.0.18", + "vitest-browser-svelte": "^2.0.2" + }, + "dependencies": { + "bancho.js": "^0.12.3", + "postgres": "^3.4.8" + } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..8f5062c --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + webServer: { command: 'npm run build && npm run preview', port: 4173 }, + testDir: 'e2e' +}); diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..d0a7b01 --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,22 @@ +// src/app.d.ts +import type { User, Session } from 'better-auth/minimal'; + +declare global { + namespace App { + interface Locals { + user?: User & { role: string }; + session?: Session; + } + + interface Error { + message: string; + code?: string; + } + + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..e57255a --- /dev/null +++ b/src/app.html @@ -0,0 +1,18 @@ + + + + + + + + + Vash Esports + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/demo.spec.ts b/src/demo.spec.ts new file mode 100644 index 0000000..e07cbbd --- /dev/null +++ b/src/demo.spec.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('sum test', () => { + it('adds 1 + 2 to equal 3', () => { + expect(1 + 2).toBe(3); + }); +}); diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..bc8ea8a --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,30 @@ +// src/hooks.server.ts +import type { Handle } from '@sveltejs/kit'; +import { building } from '$app/environment'; +import { auth } from '$lib/server/auth'; +import { svelteKitHandler } from 'better-auth/svelte-kit'; +import { db } from '$lib/server/db'; +import { user as userTable } from '$lib/server/db/auth.schema'; +import { eq } from 'drizzle-orm'; + +const handleBetterAuth: Handle = async ({ event, resolve }) => { + const session = await auth.api.getSession({ headers: event.request.headers }); + + if (session) { + event.locals.session = session.session; + + // Load fresh role from DB (better-auth doesn't include custom columns) + const dbUser = await db.query.user.findFirst({ + where: eq(userTable.id, session.user.id) + }); + + event.locals.user = { + ...session.user, + role: dbUser?.role ?? 'player' + }; + } + + return svelteKitHandler({ event, resolve, auth, building }); +}; + +export const handle: Handle = handleBetterAuth; diff --git a/src/lib/assets/favicon.svg b/src/lib/assets/favicon.svg new file mode 100644 index 0000000..cc5dc66 --- /dev/null +++ b/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte new file mode 100644 index 0000000..a0e5148 --- /dev/null +++ b/src/lib/components/Header.svelte @@ -0,0 +1,50 @@ + + +
+
+ +
+ {#if user} +
+ {#if badge} + + {badge.label} + + {/if} + {#if user.image} + {user.name} + {/if} + {user.name} +
+ +
+
+ {:else} + + Sign in with osu! + + {/if} +
+
diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte new file mode 100644 index 0000000..a1493b9 --- /dev/null +++ b/src/lib/components/Sidebar.svelte @@ -0,0 +1,83 @@ + + + diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts new file mode 100644 index 0000000..29b4f1f --- /dev/null +++ b/src/lib/server/auth.ts @@ -0,0 +1,94 @@ +// src/lib/server/auth.ts +import { betterAuth } from 'better-auth/minimal'; +import { drizzleAdapter } from 'better-auth/adapters/drizzle'; +import { sveltekitCookies } from 'better-auth/svelte-kit'; +import { env } from '$env/dynamic/private'; +import { getRequestEvent } from '$app/server'; +import { db } from '$lib/server/db'; +import { genericOAuth } from 'better-auth/plugins'; +import { team, teamMember } from './db/schema'; + +export const auth = betterAuth({ + baseURL: env.ORIGIN, + secret: env.BETTER_AUTH_SECRET, + database: drizzleAdapter(db, { provider: 'pg' }), + emailAndPassword: { enabled: true }, + databaseHooks: { + user: { + create: { + after: async (user) => { + const [personalTeam] = await db + .insert(team) + .values({ + name: user.name, + isPersonal: true, + ownerId: user.id, + avatarUrl: user.image + }) + .returning(); + + await db.insert(teamMember).values({ + teamId: personalTeam.id, + userId: user.id, + role: 'owner' + }); + } + } + } + }, + plugins: [ + genericOAuth({ + config: [ + { + providerId: 'osu', + clientId: env.OSU_CLIENT_ID!, + clientSecret: env.OSU_CLIENT_SECRET!, + authorizationUrl: 'https://osu.ppy.sh/oauth/authorize', + tokenUrl: 'https://osu.ppy.sh/oauth/token', + userInfoUrl: 'https://osu.ppy.sh/api/v2/me', + scopes: ['identify', 'public'], + pkce: false, + authentication: 'post', + // ── FIX: Use ORIGIN env var instead of hardcoded localhost ── + redirectURI: `${env.ORIGIN}/api/auth/oauth2/callback/osu`, + getToken: async ({ code, redirectURI }) => { + const res = await fetch('https://osu.ppy.sh/oauth/token', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + client_id: env.OSU_CLIENT_ID!, + client_secret: env.OSU_CLIENT_SECRET!, + code, + grant_type: 'authorization_code', + redirect_uri: redirectURI + }) + }); + const data = await res.json(); + if (!res.ok) { + console.error('osu! token exchange failed:', data); + throw new Error('Token exchange failed'); + } + return { + accessToken: data.access_token, + refreshToken: data.refresh_token, + accessTokenExpiresAt: new Date(Date.now() + data.expires_in * 1000), + scopes: ['identify', 'public'], + raw: data + }; + }, + mapProfileToUser(profile) { + return { + name: profile.username, + image: profile.avatar_url, + email: `${profile.id}@osu.local` + }; + } + } + ] + }), + sveltekitCookies(getRequestEvent) // must be last + ] +}); diff --git a/src/lib/server/bancho/client.ts b/src/lib/server/bancho/client.ts new file mode 100644 index 0000000..1df6215 --- /dev/null +++ b/src/lib/server/bancho/client.ts @@ -0,0 +1,281 @@ +import { env } from '$env/dynamic/private'; + +function sleep(ms: number) { + return new Promise((r) => setTimeout(r, ms)); +} + +// ── Singleton bancho.js connection ────────────────────────────────────── + +let client: any = null; +let connecting: Promise | null = null; + +async function getClient() { + if (client?.isConnected?.()) return client; + if (connecting) { + await connecting; + return client!; + } + + if (!env.OSU_IRC_USERNAME || !env.OSU_IRC_PASSWORD) { + throw new Error('OSU_IRC_USERNAME and OSU_IRC_PASSWORD must be set in .env'); + } + + const banchoModule = await import('bancho.js'); + const BanchoClient = + banchoModule.BanchoClient ?? + banchoModule.default?.BanchoClient ?? + banchoModule.default; + client = new BanchoClient({ + username: env.OSU_IRC_USERNAME, + password: env.OSU_IRC_PASSWORD + }); + + connecting = client.connect().then(() => { + console.log('[Bancho] Connected as', env.OSU_IRC_USERNAME); + connecting = null; + }); + + await connecting; + return client; +} + +// ── Types ─────────────────────────────────────────────────────────────── + +export interface PlayerScore { + username: string; + score: number; + passed: boolean; +} + +// ── Tournament Lobby ──────────────────────────────────────────────────── + +export class TournamentLobby { + private channel: any = null; + private channelName = ''; + public osuMatchId = 0; + + // Score collection + private collectedScores: PlayerScore[] = []; + private matchFinishedResolve: ((scores: PlayerScore[]) => void) | null = null; + + // Ready tracking + private allReadyResolve: (() => void) | null = null; + + // Event callbacks (set by orchestrator) + public onRollResult: ((username: string, value: number) => void) | null = null; + public onPickCommand: ((username: string, slotLabel: string) => void) | null = null; + + /** + * Create a new multiplayer lobby via !mp make. + */ + async create(name: string): Promise<{ matchId: number; channel: string }> { + const c = await getClient(); + + return new Promise((resolve, reject) => { + const timeout = setTimeout( + () => reject(new Error('Lobby creation timed out')), + 15000 + ); + + const banchoBot = c.getUser('BanchoBot'); + + const onPM = (msg: any) => { + const text: string = msg.message ?? String(msg); + const m = text.match(/https:\/\/osu\.ppy\.sh\/mp\/(\d+)/); + if (m) { + clearTimeout(timeout); + banchoBot.removeListener('message', onPM); + + this.osuMatchId = parseInt(m[1]); + this.channelName = `#mp_${this.osuMatchId}`; + + this.joinChannel(c).then(() => + resolve({ matchId: this.osuMatchId, channel: this.channelName }) + ); + } + }; + + banchoBot.on('message', onPM); + banchoBot.sendMessage(`!mp make ${name}`); + }); + } + + private async joinChannel(c: any) { + this.channel = c.getChannel(this.channelName); + await this.channel.join(); + + this.channel.on('message', (msg: any) => { + const text: string = msg.message ?? String(msg); + const sender: string = msg.user?.ircUsername ?? ''; + + if (sender === 'BanchoBot') { + this.parseBanchoMessage(text); + } else if (sender) { + this.parsePlayerMessage(sender, text); + } + }); + } + + // ── Message Parsing ───────────────────────────────────────────────── + + private parseBanchoMessage(text: string) { + // Player score: " finished playing (Score: 1,234,567, ... PASSED)" + const scoreMatch = text.match( + /^(.+?) finished playing \(Score: ([\d,]+).*?(PASSED|FAILED)\)/i + ); + if (scoreMatch) { + this.collectedScores.push({ + username: scoreMatch[1].trim(), + score: parseInt(scoreMatch[2].replace(/,/g, '')), + passed: scoreMatch[3] === 'PASSED' + }); + console.log(`[Bancho] Score: ${scoreMatch[1].trim()} = ${scoreMatch[2]}`); + return; + } + + // Game finished + if (text.includes('The match has finished!')) { + console.log('[Bancho] Game finished. Scores:', this.collectedScores); + if (this.matchFinishedResolve) { + this.matchFinishedResolve([...this.collectedScores]); + this.matchFinishedResolve = null; + } + this.collectedScores = []; + return; + } + + // Roll result: "Stan rolls 42 point(s)" + const rollMatch = text.match(/^(.+?) rolls (\d+) point\(s\)/); + if (rollMatch) { + const username = rollMatch[1].trim(); + const value = parseInt(rollMatch[2]); + console.log(`[Bancho] Roll: ${username} = ${value}`); + if (this.onRollResult) { + this.onRollResult(username, value); + } + return; + } + + // All players ready + if (text.includes('All players are ready')) { + console.log('[Bancho] All players ready'); + if (this.allReadyResolve) { + this.allReadyResolve(); + this.allReadyResolve = null; + } + return; + } + } + + private parsePlayerMessage(sender: string, text: string) { + // !pick NM1, !pick HD2, !pick TB, etc. + const pickMatch = text.match(/^!pick\s+([A-Z]{2})(\d*)/i); + if (pickMatch) { + const category = pickMatch[1].toUpperCase(); + const num = pickMatch[2] ? parseInt(pickMatch[2]) : 1; + const label = `${category}${num}`; + console.log(`[Bancho] Pick command from ${sender}: ${label}`); + if (this.onPickCommand) { + this.onPickCommand(sender, label); + } + return; + } + } + + // ── IRC Commands ──────────────────────────────────────────────────── + + async send(cmd: string) { + if (!this.channel) throw new Error('Lobby not created'); + console.log(`[Bancho] ${this.channelName} > ${cmd}`); + await this.channel.sendMessage(cmd); + } + + async invite(username: string) { + await this.send(`!mp invite ${username.replace(/ /g, '_')}`); + } + + async setProperties(teamMode: number, scoreMode: number, size: number) { + await this.send(`!mp set ${teamMode} ${scoreMode} ${size}`); + } + + async setMap(beatmapId: string | number, playmode = 0) { + await this.send(`!mp map ${beatmapId} ${playmode}`); + } + + async setMods(mods: string[]) { + const filtered = mods.filter((m) => !['NM', 'TB', 'None', 'NF'].includes(m)); + if (mods.includes('FM') || mods.includes('Freemod')) { + await this.send('!mp mods Freemod NF'); + } else if (filtered.length > 0) { + await this.send(`!mp mods NF ${filtered.join(' ')}`); + } else { + await this.send('!mp mods NF'); + } + } + + async startGame(delaySec = 5) { + await this.send(`!mp start ${delaySec}`); + } + + async close() { + try { + await this.send('!mp close'); + } catch { + /* lobby may already be closed */ + } + } + + async chat(message: string) { + await this.send(message); + } + + // ── Async Waiters ─────────────────────────────────────────────────── + + /** + * Wait for all players to click "Ready" in osu! client. + * BanchoBot sends "All players are ready" when they do. + */ + waitForReady(timeoutMs = 180_000): Promise { + return new Promise((resolve, reject) => { + const t = setTimeout(() => { + this.allReadyResolve = null; + reject(new Error('Timed out waiting for players to ready')); + }, timeoutMs); + this.allReadyResolve = () => { + clearTimeout(t); + resolve(); + }; + }); + } + + /** + * Wait for "The match has finished!" and return all player scores. + */ + waitForScores(timeoutMs = 600_000): Promise { + this.collectedScores = []; + return new Promise((resolve, reject) => { + const t = setTimeout(() => { + this.matchFinishedResolve = null; + reject(new Error('Timed out waiting for match scores')); + }, timeoutMs); + this.matchFinishedResolve = (scores) => { + clearTimeout(t); + resolve(scores); + }; + }); + } +} + +// ── Active lobby registry (in-memory, per server instance) ────────────── + +const lobbies = new Map(); + +export function getLobby(matchId: string) { + return lobbies.get(matchId); +} +export function setLobby(matchId: string, lobby: TournamentLobby) { + lobbies.set(matchId, lobby); +} +export function removeLobby(matchId: string) { + lobbies.delete(matchId); +} diff --git a/src/lib/server/db/auth.schema.ts b/src/lib/server/db/auth.schema.ts new file mode 100644 index 0000000..fe12e46 --- /dev/null +++ b/src/lib/server/db/auth.schema.ts @@ -0,0 +1,96 @@ +// src/lib/server/db/auth.schema.ts +import { relations } from 'drizzle-orm'; +import { pgTable, text, timestamp, boolean, index } from 'drizzle-orm/pg-core'; + +export const user = pgTable('user', { + id: text('id').primaryKey(), + name: text('name').notNull(), + email: text('email').notNull().unique(), + emailVerified: boolean('email_verified').default(false).notNull(), + image: text('image'), + // ── Staff role: 'player' | 'referee' | 'admin' ── + role: text('role').default('player').notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .$onUpdate(() => new Date()) + .notNull() +}); + +export const session = pgTable( + 'session', + { + id: text('id').primaryKey(), + expiresAt: timestamp('expires_at').notNull(), + token: text('token').notNull().unique(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .$onUpdate(() => new Date()) + .notNull(), + ipAddress: text('ip_address'), + userAgent: text('user_agent'), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }) + }, + (table) => [index('session_userId_idx').on(table.userId)] +); + +export const account = pgTable( + 'account', + { + id: text('id').primaryKey(), + accountId: text('account_id').notNull(), + providerId: text('provider_id').notNull(), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + accessToken: text('access_token'), + refreshToken: text('refresh_token'), + idToken: text('id_token'), + accessTokenExpiresAt: timestamp('access_token_expires_at'), + refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), + scope: text('scope'), + password: text('password'), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .$onUpdate(() => new Date()) + .notNull() + }, + (table) => [index('account_userId_idx').on(table.userId)] +); + +export const verification = pgTable( + 'verification', + { + id: text('id').primaryKey(), + identifier: text('identifier').notNull(), + value: text('value').notNull(), + expiresAt: timestamp('expires_at').notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .$onUpdate(() => new Date()) + .notNull() + }, + (table) => [index('verification_identifier_idx').on(table.identifier)] +); + +export const userRelations = relations(user, ({ many }) => ({ + sessions: many(session), + accounts: many(account) +})); + +export const sessionRelations = relations(session, ({ one }) => ({ + user: one(user, { + fields: [session.userId], + references: [user.id] + }) +})); + +export const accountRelations = relations(account, ({ one }) => ({ + user: one(user, { + fields: [account.userId], + references: [user.id] + }) +})); diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts new file mode 100644 index 0000000..fb8b355 --- /dev/null +++ b/src/lib/server/db/index.ts @@ -0,0 +1,10 @@ +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import * as schema from './schema'; +import { env } from '$env/dynamic/private'; + +if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set'); + +const client = postgres(env.DATABASE_URL); + +export const db = drizzle(client, { schema }); diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts new file mode 100644 index 0000000..9fd37a1 --- /dev/null +++ b/src/lib/server/db/schema.ts @@ -0,0 +1,249 @@ +// src/lib/server/db/schema.ts +// +// CHANGE: Add `role` column to user table in auth.schema.ts (see that file). +// This file adds the permission helper utilities. + +import { + pgTable, + integer, + text, + uuid, + boolean, + timestamp, + jsonb, + index, + uniqueIndex, + real +} from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; + +export * from './auth.schema'; + +// ── Teams ─────────────────────────────────────────────────────────────── +export const team = pgTable('team', { + id: uuid('id').primaryKey().defaultRandom(), + name: text('name').notNull(), + isPersonal: boolean('is_personal').default(false).notNull(), + ownerId: text('owner_id'), + avatarUrl: text('avatar_url'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull() +}); + +// ── Team Members ──────────────────────────────────────────────────────── +export const teamMember = pgTable('team_member', { + id: uuid('id').primaryKey().defaultRandom(), + teamId: uuid('team_id') + .notNull() + .references(() => team.id, { onDelete: 'cascade' }), + userId: text('user_id').notNull(), + role: text('role').default('member'), + joinedAt: timestamp('joined_at', { withTimezone: true }).defaultNow() +}); + +// ── Mappools ──────────────────────────────────────────────────────────── +export const mappool = pgTable('mappool', { + id: uuid('id').primaryKey().defaultRandom(), + name: text('name').notNull(), + createdBy: text('created_by'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull() +}); + +// ── Mappool Slots ─────────────────────────────────────────────────────── +export const mappoolSlot = pgTable('mappool_slot', { + id: uuid('id').primaryKey().defaultRandom(), + mappoolId: uuid('mappool_id') + .notNull() + .references(() => mappool.id, { onDelete: 'cascade' }), + category: text('category').notNull(), + orderInCategory: integer('order_in_category').notNull(), + beatmapId: text('beatmap_id').notNull(), + starRating: real('star_rating'), + mods: text('mods').array().default([]).notNull() +}); + +// ── Matches ───────────────────────────────────────────────────────────── +export const match = pgTable( + 'match', + { + id: uuid('id').primaryKey().defaultRandom(), + name: text('name'), + state: text('state').default('CREATED').notNull(), + config: jsonb('config').notNull(), + mappoolId: uuid('mappool_id').references(() => mappool.id), + osuLobbyId: integer('osu_lobby_id'), + scheduledAt: timestamp('scheduled_at', { withTimezone: true }), + startedAt: timestamp('started_at', { withTimezone: true }), + finishedAt: timestamp('finished_at', { withTimezone: true }), + winnerId: uuid('winner_id').references(() => team.id), + createdBy: text('created_by'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull() + }, + (t) => [index('idx_match_state').on(t.state)] +); + +// ── Match Participants (a team in a match) ────────────────────────────── +export const matchParticipant = pgTable( + 'match_participant', + { + id: uuid('id').primaryKey().defaultRandom(), + matchId: uuid('match_id') + .notNull() + .references(() => match.id, { onDelete: 'cascade' }), + teamId: uuid('team_id') + .notNull() + .references(() => team.id), + slot: integer('slot').notNull(), + score: integer('score').default(0).notNull(), + rollValue: integer('roll_value'), + pickOrder: integer('pick_order') + }, + (t) => [ + index('idx_mp_match_team').on(t.matchId, t.teamId), + uniqueIndex('idx_mp_match_slot').on(t.matchId, t.slot), + index('idx_mp_match').on(t.matchId) + ] +); + +// ── Match Participant Players (a player fielded for this match) ───────── +export const matchParticipantPlayer = pgTable( + 'match_participant_player', + { + id: uuid('id').primaryKey().defaultRandom(), + participantId: uuid('participant_id') + .notNull() + .references(() => matchParticipant.id, { onDelete: 'cascade' }), + userId: text('user_id').notNull() + }, + (t) => [ + uniqueIndex('idx_mpp_participant_user').on(t.participantId, t.userId), + index('idx_mpp_participant').on(t.participantId) + ] +); + +// ── Match Games ───────────────────────────────────────────────────────── +export const matchGame = pgTable( + 'match_game', + { + id: uuid('id').primaryKey().defaultRandom(), + matchId: uuid('match_id') + .notNull() + .references(() => match.id, { onDelete: 'cascade' }), + gameNumber: integer('game_number').notNull(), + mappoolSlotId: uuid('mappool_slot_id') + .notNull() + .references(() => mappoolSlot.id), + pickedByParticipantId: uuid('picked_by_participant_id').references(() => matchParticipant.id), + winnerParticipantId: uuid('winner_participant_id').references(() => matchParticipant.id), + state: text('state').default('PENDING').notNull(), + startedAt: timestamp('started_at', { withTimezone: true }), + finishedAt: timestamp('finished_at', { withTimezone: true }) + }, + (t) => [index('idx_match_game_match').on(t.matchId)] +); + +// ── Match Game Scores (per-player per-map) ────────────────────────────── +export const matchGameScore = pgTable( + 'match_game_score', + { + id: uuid('id').primaryKey().defaultRandom(), + matchGameId: uuid('match_game_id') + .notNull() + .references(() => matchGame.id, { onDelete: 'cascade' }), + playerId: uuid('player_id') + .notNull() + .references(() => matchParticipantPlayer.id), + score: integer('score').default(0).notNull(), + accuracy: real('accuracy').default(0).notNull(), + maxCombo: integer('max_combo').default(0).notNull(), + count300: integer('count_300').default(0).notNull(), + count100: integer('count_100').default(0).notNull(), + count50: integer('count_50').default(0).notNull(), + countMiss: integer('count_miss').default(0).notNull(), + mods: text('mods').array().default([]).notNull(), + passed: boolean('passed').default(false).notNull() + }, + (t) => [index('idx_mgs_game').on(t.matchGameId)] +); + +// ── Match Queue ───────────────────────────────────────────────────────── +export const matchQueue = pgTable( + 'match_queue', + { + id: uuid('id').primaryKey().defaultRandom(), + userId: text('user_id').notNull(), + teamId: uuid('team_id') + .notNull() + .references(() => team.id), + elo: integer('elo').default(1000).notNull(), + joinedAt: timestamp('joined_at', { withTimezone: true }).defaultNow().notNull() + }, + (t) => [ + uniqueIndex('idx_queue_user').on(t.userId), + index('idx_queue_elo').on(t.elo) + ] +); + +// ── Player ELO ────────────────────────────────────────────────────────── +export const playerRating = pgTable('player_rating', { + id: uuid('id').primaryKey().defaultRandom(), + userId: text('user_id').notNull().unique(), + elo: integer('elo').default(1000).notNull(), + wins: integer('wins').default(0).notNull(), + losses: integer('losses').default(0).notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() +}); + +// ── Relations ─────────────────────────────────────────────────────────── + +export const teamRelations = relations(team, ({ many }) => ({ + members: many(teamMember) +})); + +export const teamMemberRelations = relations(teamMember, ({ one }) => ({ + team: one(team, { fields: [teamMember.teamId], references: [team.id] }) +})); + +export const mappoolRelations = relations(mappool, ({ many }) => ({ + slots: many(mappoolSlot) +})); + +export const mappoolSlotRelations = relations(mappoolSlot, ({ one }) => ({ + mappool: one(mappool, { fields: [mappoolSlot.mappoolId], references: [mappool.id] }) +})); + +export const matchRelations = relations(match, ({ one, many }) => ({ + mappool: one(mappool, { fields: [match.mappoolId], references: [mappool.id] }), + participants: many(matchParticipant), + games: many(matchGame) +})); + +export const matchParticipantRelations = relations(matchParticipant, ({ one, many }) => ({ + match: one(match, { fields: [matchParticipant.matchId], references: [match.id] }), + team: one(team, { fields: [matchParticipant.teamId], references: [team.id] }), + players: many(matchParticipantPlayer) +})); + +export const matchParticipantPlayerRelations = relations(matchParticipantPlayer, ({ one }) => ({ + participant: one(matchParticipant, { + fields: [matchParticipantPlayer.participantId], + references: [matchParticipant.id] + }) +})); + +export const matchGameRelations = relations(matchGame, ({ one, many }) => ({ + match: one(match, { fields: [matchGame.matchId], references: [match.id] }), + slot: one(mappoolSlot, { fields: [matchGame.mappoolSlotId], references: [mappoolSlot.id] }), + scores: many(matchGameScore) +})); + +export const matchGameScoreRelations = relations(matchGameScore, ({ one }) => ({ + game: one(matchGame, { fields: [matchGameScore.matchGameId], references: [matchGame.id] }), + player: one(matchParticipantPlayer, { + fields: [matchGameScore.playerId], + references: [matchParticipantPlayer.id] + }) +})); + +export const matchQueueRelations = relations(matchQueue, ({ one }) => ({ + team: one(team, { fields: [matchQueue.teamId], references: [team.id] }) +})); diff --git a/src/lib/server/match/engine.ts b/src/lib/server/match/engine.ts new file mode 100644 index 0000000..7c24eff --- /dev/null +++ b/src/lib/server/match/engine.ts @@ -0,0 +1,523 @@ +import { db } from '$lib/server/db'; +import { + match, + matchParticipant, + matchParticipantPlayer, + matchGame, + matchGameScore, + matchQueue, + playerRating, + teamMember, +} from '$lib/server/db/schema'; +import { eq, asc, sql } from 'drizzle-orm'; +import { MATCH_STATES, GAME_STATES, type MatchConfig } from './types'; + +// ── Queue ─────────────────────────────────────────────────────────────── + +export async function joinQueue(userId: string, teamId: string) { + let rating = await db.query.playerRating.findFirst({ + where: eq(playerRating.userId, userId) + }); + + if (!rating) { + [rating] = await db + .insert(playerRating) + .values({ userId, elo: 1000 }) + .returning(); + } + + const existing = await db.query.matchQueue.findFirst({ + where: eq(matchQueue.userId, userId) + }); + + if (existing) throw new Error('Already in queue'); + + await db.insert(matchQueue).values({ + userId, + teamId, + elo: rating.elo + }); + + return tryMatchFromQueue(); +} + +export async function leaveQueue(userId: string) { + await db.delete(matchQueue).where(eq(matchQueue.userId, userId)); +} + +export async function getQueueStatus(userId: string) { + const entry = await db.query.matchQueue.findFirst({ + where: eq(matchQueue.userId, userId) + }); + + const count = await db + .select({ count: sql`count(*)` }) + .from(matchQueue); + + return { + inQueue: !!entry, + queueSize: Number(count[0].count), + joinedAt: entry?.joinedAt ?? null + }; +} + +async function tryMatchFromQueue() { + const queue = await db.query.matchQueue.findMany({ + orderBy: asc(matchQueue.elo) + }); + + if (queue.length < 2) return null; + + // Find closest ELO pair + let bestPair: [typeof queue[0], typeof queue[0]] | null = null; + let smallestGap = Infinity; + + for (let i = 0; i < queue.length - 1; i++) { + const gap = Math.abs(queue[i].elo - queue[i + 1].elo); + if (gap < smallestGap) { + smallestGap = gap; + bestPair = [queue[i], queue[i + 1]]; + } + } + + if (!bestPair) return null; + + // Pick mappool based on average player rating + const avgElo = (bestPair[0].elo + bestPair[1].elo) / 2; + const selectedPool = await selectMappoolForRating(avgElo); + + if (!selectedPool) throw new Error('No mappools available'); + + // Remove both from queue + await db.delete(matchQueue).where(eq(matchQueue.userId, bestPair[0].userId)); + await db.delete(matchQueue).where(eq(matchQueue.userId, bestPair[1].userId)); + + const created = await createMatch({ + name: 'Ranked Match', + config: { bestOf: 7, teamSize: 1, scoringType: 'score' }, + mappoolId: selectedPool.id, + teams: [bestPair[0].teamId, bestPair[1].teamId], + createdBy: 'system' + }); + + return created; +} + +async function selectMappoolForRating(avgElo: number) { + // Get all mappools with their slots + const pools = await db.query.mappool.findMany({ + with: { slots: true } + }); + + if (pools.length === 0) return null; + + // Map ELO range (e.g. 800-1200) to star rating range (e.g. 3-7) + // Simple linear: ELO 800 → 3★, ELO 1200 → 7★ + const targetStars = 3 + ((avgElo - 800) / 400) * 4; + const clampedTarget = Math.max(2, Math.min(8, targetStars)); + + let bestPool = pools[0]; + let bestDiff = Infinity; + + for (const pool of pools) { + if (pool.slots.length === 0) continue; + + const avgStars = + pool.slots.reduce((sum, s) => sum + (s.starRating ?? 0), 0) / pool.slots.length; + const diff = Math.abs(avgStars - clampedTarget); + + if (diff < bestDiff) { + bestDiff = diff; + bestPool = pool; + } + } + + return bestPool; +} + +// ── Create Match ──────────────────────────────────────────────────────── + +export async function createMatch(opts: { + name: string; + config: MatchConfig; + mappoolId: string; + teams: string[]; + createdBy: string; +}) { + const { name, config, mappoolId, teams, createdBy } = opts; + + const [created] = await db + .insert(match) + .values({ + name, + config, + mappoolId, + createdBy, + state: MATCH_STATES.CREATED + }) + .returning(); + + for (let i = 0; i < teams.length; i++) { + const [participant] = await db + .insert(matchParticipant) + .values({ + matchId: created.id, + teamId: teams[i], + slot: i + 1 + }) + .returning(); + + const members = await db.query.teamMember.findMany({ + where: eq(teamMember.teamId, teams[i]) + }); + + for (const member of members) { + await db.insert(matchParticipantPlayer).values({ + participantId: participant.id, + userId: member.userId + }); + } + } + + return getMatchFull(created.id); +} + +// ── State Transitions ─────────────────────────────────────────────────── + +export async function moveToLobby(matchId: string, osuLobbyId?: number) { + const m = await getMatchOrThrow(matchId); + assertState(m.state, MATCH_STATES.CREATED); + + await db + .update(match) + .set({ + state: MATCH_STATES.LOBBY, + osuLobbyId: osuLobbyId ?? null, + startedAt: new Date() + }) + .where(eq(match.id, matchId)); + + return getMatchFull(matchId); +} + +export async function moveToRolling(matchId: string) { + const m = await getMatchOrThrow(matchId); + assertState(m.state, MATCH_STATES.LOBBY); + + await db + .update(match) + .set({ state: MATCH_STATES.ROLLING }) + .where(eq(match.id, matchId)); + + return getMatchFull(matchId); +} + +// ── Roll ──────────────────────────────────────────────────────────────── + +export async function submitRoll(matchId: string, participantId: string, value: number) { + const m = await getMatchOrThrow(matchId); + assertState(m.state, MATCH_STATES.ROLLING); + + await db + .update(matchParticipant) + .set({ rollValue: value }) + .where(eq(matchParticipant.id, participantId)); + + const participants = await db.query.matchParticipant.findMany({ + where: eq(matchParticipant.matchId, matchId) + }); + + const allRolled = participants.every( + (p) => (p.id === participantId ? true : p.rollValue !== null) + ); + + if (allRolled) { + const sorted = [...participants] + .map((p) => ({ + ...p, + rollValue: p.id === participantId ? value : p.rollValue! + })) + .sort((a, b) => b.rollValue - a.rollValue); + + for (let i = 0; i < sorted.length; i++) { + await db + .update(matchParticipant) + .set({ pickOrder: i + 1 }) + .where(eq(matchParticipant.id, sorted[i].id)); + } + + await db + .update(match) + .set({ state: MATCH_STATES.PICKING }) + .where(eq(match.id, matchId)); + } + + return getMatchFull(matchId); +} + +// ── Pick Map ──────────────────────────────────────────────────────────── + +export async function pickMap(matchId: string, participantId: string, mappoolSlotId: string) { + const m = await getMatchOrThrow(matchId); + assertState(m.state, MATCH_STATES.PICKING); + + const participants = await db.query.matchParticipant.findMany({ + where: eq(matchParticipant.matchId, matchId) + }); + + const totalGames = await db.query.matchGame.findMany({ + where: eq(matchGame.matchId, matchId) + }); + + const expectedPicker = getExpectedPicker(participants, totalGames.length); + if (expectedPicker.id !== participantId) { + throw new Error('Not your turn to pick'); + } + + const alreadyPlayed = totalGames.some((g) => g.mappoolSlotId === mappoolSlotId); + if (alreadyPlayed) { + throw new Error('This map has already been played'); + } + + const [game] = await db + .insert(matchGame) + .values({ + matchId, + gameNumber: totalGames.length + 1, + mappoolSlotId, + pickedByParticipantId: participantId, + state: GAME_STATES.PLAYING, + startedAt: new Date() + }) + .returning(); + + await db + .update(match) + .set({ state: MATCH_STATES.PLAYING }) + .where(eq(match.id, matchId)); + + return game; +} + +// ── Submit Scores ─────────────────────────────────────────────────────── + +export async function submitGameScores( + matchGameId: string, + scores: { + playerId: string; + score: number; + accuracy?: number; + maxCombo?: number; + count300?: number; + count100?: number; + count50?: number; + countMiss?: number; + mods?: string[]; + passed?: boolean; + }[] +) { + const game = await db.query.matchGame.findFirst({ + where: eq(matchGame.id, matchGameId) + }); + + if (!game) throw new Error('Game not found'); + if (game.state !== GAME_STATES.PLAYING) throw new Error(`Game is ${game.state}, not PLAYING`); + + for (const s of scores) { + await db.insert(matchGameScore).values({ + matchGameId, + playerId: s.playerId, + score: s.score, + accuracy: s.accuracy ?? 0, + maxCombo: s.maxCombo ?? 0, + count300: s.count300 ?? 0, + count100: s.count100 ?? 0, + count50: s.count50 ?? 0, + countMiss: s.countMiss ?? 0, + mods: s.mods ?? [], + passed: s.passed ?? true + }); + } + + const allScores = await db.query.matchGameScore.findMany({ + where: eq(matchGameScore.matchGameId, matchGameId), + with: { player: true } + }); + + const participantScores = new Map(); + for (const s of allScores) { + const pid = s.player.participantId; + participantScores.set(pid, (participantScores.get(pid) ?? 0) + s.score); + } + + let winnerId: string | null = null; + let highScore = -1; + for (const [pid, total] of participantScores) { + if (total > highScore) { + highScore = total; + winnerId = pid; + } + } + + await db + .update(matchGame) + .set({ + state: GAME_STATES.FINISHED, + winnerParticipantId: winnerId, + finishedAt: new Date() + }) + .where(eq(matchGame.id, matchGameId)); + + if (winnerId) { + const participant = await db.query.matchParticipant.findFirst({ + where: eq(matchParticipant.id, winnerId) + }); + if (participant) { + await db + .update(matchParticipant) + .set({ score: participant.score + 1 }) + .where(eq(matchParticipant.id, winnerId)); + } + } + + const m = await getMatchOrThrow(game.matchId); + const config = m.config as MatchConfig; + const winsNeeded = Math.ceil(config.bestOf / 2); + + const participants = await db.query.matchParticipant.findMany({ + where: eq(matchParticipant.matchId, game.matchId) + }); + + const matchWinner = participants.find((p) => p.score >= winsNeeded); + + if (matchWinner) { + await db + .update(match) + .set({ + state: MATCH_STATES.FINISHED, + winnerId: matchWinner.teamId, + finishedAt: new Date() + }) + .where(eq(match.id, game.matchId)); + + await updateElo(participants, matchWinner.id); + } else { + await db + .update(match) + .set({ state: MATCH_STATES.PICKING }) + .where(eq(match.id, game.matchId)); + } + + return getMatchFull(game.matchId); +} + +// ── ELO ───────────────────────────────────────────────────────────────── + +async function updateElo( + participants: { id: string; teamId: string }[], + winnerId: string +) { + for (const p of participants) { + const players = await db.query.matchParticipantPlayer.findMany({ + where: eq(matchParticipantPlayer.participantId, p.id) + }); + + const isWinner = p.id === winnerId; + + for (const player of players) { + let rating = await db.query.playerRating.findFirst({ + where: eq(playerRating.userId, player.userId) + }); + + if (!rating) { + [rating] = await db + .insert(playerRating) + .values({ userId: player.userId, elo: 1000 }) + .returning(); + } + + const K = 32; + const expected = 0.5; + const actual = isWinner ? 1 : 0; + const newElo = Math.round(rating.elo + K * (actual - expected)); + + await db + .update(playerRating) + .set({ + elo: newElo, + wins: isWinner ? rating.wins + 1 : rating.wins, + losses: isWinner ? rating.losses : rating.losses + 1, + updatedAt: new Date() + }) + .where(eq(playerRating.userId, player.userId)); + } + } +} + +// ── Cancel Match ──────────────────────────────────────────────────────── + +export async function cancelMatch(matchId: string) { + await db + .update(match) + .set({ + state: MATCH_STATES.CANCELLED, + finishedAt: new Date() + }) + .where(eq(match.id, matchId)); + + return getMatchFull(matchId); +} + +// ── Helpers ───────────────────────────────────────────────────────────── + +function getExpectedPicker( + participants: { id: string; pickOrder: number | null; score: number }[], + gameCount: number +) { + const sorted = [...participants].sort((a, b) => (a.pickOrder ?? 99) - (b.pickOrder ?? 99)); + const pickerIndex = gameCount % sorted.length; + return sorted[pickerIndex]; +} + +export async function getMatchFull(matchId: string) { + const m = await db.query.match.findFirst({ + where: eq(match.id, matchId), + with: { + mappool: { + with: { slots: true } + }, + participants: { + with: { + team: true, + players: true + } + }, + games: { + with: { + slot: true, + scores: { + with: { player: true } + } + }, + orderBy: (g, { asc }) => [asc(g.gameNumber)] + } + } + }); + + if (!m) throw new Error('Match not found'); + return m; +} + +async function getMatchOrThrow(matchId: string) { + const m = await db.query.match.findFirst({ + where: eq(match.id, matchId) + }); + if (!m) throw new Error('Match not found'); + return m; +} + +function assertState(current: string, expected: string) { + if (current !== expected) { + throw new Error(`Match is ${current}, expected ${expected}`); + } +} diff --git a/src/lib/server/match/orchestrator.ts b/src/lib/server/match/orchestrator.ts new file mode 100644 index 0000000..4ed3967 --- /dev/null +++ b/src/lib/server/match/orchestrator.ts @@ -0,0 +1,331 @@ +import { db } from '$lib/server/db'; +import { match, matchGame, mappoolSlot, user } from '$lib/server/db/schema'; +import { eq, and } from 'drizzle-orm'; +import { MATCH_STATES, type MatchConfig } from './types'; +import { getMatchFull, submitRoll, pickMap, submitGameScores } from './engine'; +import { TournamentLobby, setLobby, getLobby, removeLobby } from '../bancho/client'; + +function sleep(ms: number) { + return new Promise((r) => setTimeout(r, ms)); +} + +/** + * Create IRC lobby → configure → invite players → wire up chat handlers → move to ROLLING. + */ +export async function initMatchLobby(matchId: string) { + const m = await getMatchFull(matchId); + const config = m.config as MatchConfig; + const lobby = new TournamentLobby(); + + const lobbyName = `VASH: (${m.participants[0]?.team.name}) vs (${m.participants[1]?.team.name})`; + console.log(`[Orchestrator] Creating lobby: ${lobbyName}`); + + const { matchId: osuId, channel } = await lobby.create(lobbyName); + console.log(`[Orchestrator] Created ${channel} (osu mp/${osuId})`); + + setLobby(matchId, lobby); + + // CREATED → LOBBY + await db + .update(match) + .set({ state: MATCH_STATES.LOBBY, osuLobbyId: osuId, startedAt: new Date() }) + .where(eq(match.id, matchId)); + + // Configure: HeadToHead, correct score mode, correct size + const scoreMode = + config.scoringType === 'score_v2' ? 3 + : config.scoringType === 'accuracy' ? 1 + : config.scoringType === 'combo' ? 2 + : 0; + await lobby.setProperties(0, scoreMode, config.teamSize * 2); + + // Remove password so players can join via mp link (and rejoin after disconnect) + await lobby.send('!mp password'); + await sleep(500); + + // Invite all players from both teams (with dedup + delays) + const invited = new Set(); + for (const p of m.participants) { + for (const pl of p.players) { + const u = await db.query.user.findFirst({ where: eq(user.id, pl.userId) }); + if (u?.name && !invited.has(u.name.toLowerCase())) { + await lobby.invite(u.name); + invited.add(u.name.toLowerCase()); + console.log(`[Orchestrator] Invited ${u.name}`); + await sleep(300); + } + } + } + + // Wire up IRC chat handlers for rolls and picks + setupChatHandlers(matchId, lobby); + + await lobby.chat( + `Welcome! BO${config.bestOf}. Type !roll to decide pick order, or use the web UI.` + ); + + // LOBBY → ROLLING + await db.update(match).set({ state: MATCH_STATES.ROLLING }).where(eq(match.id, matchId)); + + return { osuMatchId: osuId, channel }; +} + +/** + * Wire up IRC event callbacks so players can !roll and !pick from osu! chat. + */ +function setupChatHandlers(matchId: string, lobby: TournamentLobby) { + // ── Handle !roll from IRC ─────────────────────────────────────────── + lobby.onRollResult = async (username: string, value: number) => { + try { + const m = await getMatchFull(matchId); + if (m.state !== MATCH_STATES.ROLLING) { + await lobby.chat(`Rolls are not active right now.`); + return; + } + + // Find participant linked to this username + let targetParticipant: { id: string; rollValue: number | null } | null = null; + + for (const p of m.participants) { + for (const pl of p.players) { + const u = await db.query.user.findFirst({ where: eq(user.id, pl.userId) }); + if (u?.name?.toLowerCase() === username.toLowerCase().replace(/_/g, ' ')) { + // If this participant already rolled, try finding another one (same-team testing) + if (p.rollValue === null) { + targetParticipant = p; + break; + } else if (!targetParticipant) { + // Keep looking for an unrolled one + const unrolled = m.participants.find((pp) => pp.rollValue === null); + if (unrolled) targetParticipant = unrolled; + } + } + } + if (targetParticipant?.rollValue === null) break; + } + + if (!targetParticipant || targetParticipant.rollValue !== null) { + await lobby.chat(`${username}: already rolled or not in this match.`); + return; + } + + await submitRoll(matchId, targetParticipant.id, value); + console.log(`[Orchestrator] IRC roll: ${username} = ${value}`); + + // Check if all rolled → announce pick order + const updated = await getMatchFull(matchId); + if (updated.state === MATCH_STATES.PICKING) { + const sorted = [...updated.participants].sort( + (a, b) => (a.pickOrder ?? 99) - (b.pickOrder ?? 99) + ); + await lobby.chat( + `Rolls complete! ${sorted[0]?.team.name} picks first. Use !pick (e.g. !pick NM1) or pick in web UI.` + ); + } + } catch (err: any) { + console.error('[Orchestrator] IRC roll error:', err.message); + } + }; + + // ── Handle !pick from IRC ─────────────────────────────────────────── + lobby.onPickCommand = async (username: string, slotLabel: string) => { + try { + const m = await getMatchFull(matchId); + if (m.state !== MATCH_STATES.PICKING) { + await lobby.chat(`Picks are not active right now.`); + return; + } + + // Parse slot label: "NM1" → category=NM, orderInCategory=1 + const labelMatch = slotLabel.match(/^([A-Z]{2})(\d+)$/); + if (!labelMatch) { + await lobby.chat(`${username}: Invalid slot. Use format like NM1, HD2, DT1.`); + return; + } + const category = labelMatch[1]; + const order = parseInt(labelMatch[2]); + + // Find the expected picker + const sorted = [...m.participants].sort( + (a, b) => (a.pickOrder ?? 99) - (b.pickOrder ?? 99) + ); + const expectedIdx = m.games.length % sorted.length; + const expectedPicker = sorted[expectedIdx]; + + // Verify it's this player's turn to pick + let isPickerTurn = false; + if (expectedPicker) { + for (const pl of expectedPicker.players) { + const u = await db.query.user.findFirst({ where: eq(user.id, pl.userId) }); + if (u?.name?.toLowerCase() === username.toLowerCase().replace(/_/g, ' ')) { + isPickerTurn = true; + break; + } + } + } + + // For same-team testing, allow any participant + if (!isPickerTurn && m.participants[0]?.teamId === m.participants[1]?.teamId) { + isPickerTurn = true; + } + + if (!isPickerTurn) { + await lobby.chat(`${username}: It's not your turn to pick.`); + return; + } + + // Find the mappool slot + const slot = m.mappool?.slots?.find( + (s) => s.category === category && s.orderInCategory === order + ); + if (!slot) { + await lobby.chat(`${username}: Slot ${slotLabel} not found in this mappool.`); + return; + } + + // Check if already played + const alreadyPlayed = m.games.some((g) => g.mappoolSlotId === slot.id); + if (alreadyPlayed) { + await lobby.chat(`${username}: ${slotLabel} has already been played.`); + return; + } + + // Execute the pick + const game = await pickMap(matchId, expectedPicker!.id, slot.id); + console.log(`[Orchestrator] IRC pick: ${username} picked ${slotLabel}`); + + // Set map in lobby and wait for ready + playPickedMap(matchId, game.id).catch((err) => + console.error('[Orchestrator] IRC play failed:', err.message) + ); + } catch (err: any) { + console.error('[Orchestrator] IRC pick error:', err.message); + await lobby.chat(`Error: ${err.message}`).catch(() => {}); + } + }; +} + +/** + * Called after a map is picked (from web UI or IRC). + * Sets map + mods in IRC, waits for ready, starts game, collects scores. + */ +export async function playPickedMap(matchId: string, matchGameId: string) { + const lobby = getLobby(matchId); + if (!lobby) { + console.warn('[Orchestrator] No IRC lobby for', matchId, '— skipping IRC'); + return; + } + + const game = await db.query.matchGame.findFirst({ + where: eq(matchGame.id, matchGameId), + with: { slot: true } + }); + if (!game) throw new Error('Game not found'); + + const slot = game.slot; + + // Set beatmap + await lobby.setMap(slot.beatmapId); + await sleep(1000); + + // Set mods (always NF + slot mods) + await lobby.setMods(slot.mods); + await sleep(500); + + await lobby.chat( + `Playing ${slot.category}${slot.orderInCategory}. Ready up! (Game starts when all players are ready)` + ); + + // Wait for all players to ready up in osu!, then start + try { + await lobby.waitForReady(); + await lobby.chat('All ready — starting in 5s!'); + await lobby.startGame(5); + } catch (err: any) { + // If ready times out, inform and don't start + console.warn('[Orchestrator] Ready timeout:', err.message); + await lobby.chat('Ready timed out. Use the web UI to force start, or ready up!'); + return; + } + + // Non-blocking: wait for scores then process + collectScores(matchId, matchGameId, lobby).catch((err) => + console.error('[Orchestrator] Score collection failed:', err) + ); +} + +/** + * Force start the current game without waiting for ready. + * Called from the web UI "Force Start" button. + */ +export async function forceStartGame(matchId: string) { + const lobby = getLobby(matchId); + if (!lobby) return; + + await lobby.chat('Force starting in 10s!'); + await lobby.startGame(10); +} + +/** + * Background task: waits for IRC scores, writes them to DB, advances match state. + */ +async function collectScores(matchId: string, matchGameId: string, lobby: TournamentLobby) { + const ircScores = await lobby.waitForScores(); + console.log(`[Orchestrator] Scores for game ${matchGameId}:`, ircScores); + + // Map IRC usernames → matchParticipantPlayer IDs + const m = await getMatchFull(matchId); + const scores: { playerId: string; score: number; passed: boolean }[] = []; + + for (const irc of ircScores) { + for (const participant of m.participants) { + for (const player of participant.players) { + const u = await db.query.user.findFirst({ where: eq(user.id, player.userId) }); + // IRC uses underscores for spaces in usernames + const ircName = irc.username.toLowerCase().replace(/_/g, ' '); + const dbName = u?.name?.toLowerCase(); + if (dbName && (dbName === ircName || dbName === irc.username.toLowerCase())) { + scores.push({ playerId: player.id, score: irc.score, passed: irc.passed }); + } + } + } + } + + if (scores.length === 0) { + console.warn('[Orchestrator] No scores matched any IRC usernames'); + return; + } + + await submitGameScores(matchGameId, scores); + + // Check if match finished or needs next pick + const updated = await getMatchFull(matchId); + if (updated.state === MATCH_STATES.FINISHED) { + const winner = updated.participants.find((p) => p.teamId === updated.winnerId); + await lobby.chat(`GG! ${winner?.team.name ?? '?'} wins the match!`); + await sleep(5000); + await lobby.close(); + removeLobby(matchId); + } else { + // Announce next picker + const sorted = [...updated.participants].sort( + (a, b) => (a.pickOrder ?? 99) - (b.pickOrder ?? 99) + ); + const nextIdx = updated.games.length % sorted.length; + const nextPicker = sorted[nextIdx]; + await lobby.chat( + `${nextPicker?.team.name}'s turn to pick. Use !pick (e.g. !pick HD1) or pick in web UI.` + ); + } +} + +/** + * Close the IRC lobby for a match. + */ +export async function closeLobby(matchId: string) { + const lobby = getLobby(matchId); + if (lobby) { + await lobby.close(); + removeLobby(matchId); + } +} diff --git a/src/lib/server/match/types.ts b/src/lib/server/match/types.ts new file mode 100644 index 0000000..759b473 --- /dev/null +++ b/src/lib/server/match/types.ts @@ -0,0 +1,24 @@ +export interface MatchConfig { + bestOf: number; + teamSize: number; + scoringType: 'score' | 'score_v2' | 'accuracy' | 'combo'; +} + +export const MATCH_STATES = { + CREATED: 'CREATED', + LOBBY: 'LOBBY', + ROLLING: 'ROLLING', + PICKING: 'PICKING', + PLAYING: 'PLAYING', + FINISHED: 'FINISHED', + CANCELLED: 'CANCELLED' +} as const; + +export const GAME_STATES = { + PENDING: 'PENDING', + PLAYING: 'PLAYING', + FINISHED: 'FINISHED' +} as const; + +export type MatchState = (typeof MATCH_STATES)[keyof typeof MATCH_STATES]; +export type GameState = (typeof GAME_STATES)[keyof typeof GAME_STATES]; diff --git a/src/lib/server/osu/api.ts b/src/lib/server/osu/api.ts new file mode 100644 index 0000000..3dfa4ac --- /dev/null +++ b/src/lib/server/osu/api.ts @@ -0,0 +1,136 @@ +// src/lib/server/osu/api.ts +import { env } from '$env/dynamic/private'; +import { db } from '$lib/server/db'; +import { and, eq } from 'drizzle-orm'; +import { account } from '$lib/server/db/auth.schema'; +import { getCached, setCache } from './cache'; + +let clientToken: string | null = null; +let clientTokenExpiresAt = 0; + +async function getClientToken(): Promise { + if (clientToken && Date.now() < clientTokenExpiresAt - 60_000) { + return clientToken; + } + + const res = await fetch('https://osu.ppy.sh/oauth/token', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + client_id: env.OSU_CLIENT_ID!, + client_secret: env.OSU_CLIENT_SECRET!, + grant_type: 'client_credentials', + scope: 'identify public' + }) + }); + + if (!res.ok) { + throw new Error(`Failed to get osu! client token: ${res.status} ${res.statusText}`); + } + + const data = await res.json(); + clientToken = data.access_token; + clientTokenExpiresAt = Date.now() + data.expires_in * 1000; + return clientToken!; +} + +async function getUserToken(userId: string): Promise { + const acc = await db.query.account.findFirst({ + where: and(eq(account.userId, userId), eq(account.providerId, 'osu')) + }); + + if (!acc?.accessToken) return null; + + if (acc.accessTokenExpiresAt && acc.accessTokenExpiresAt < new Date()) { + if (!acc.refreshToken) return null; + + const res = await fetch('https://osu.ppy.sh/oauth/token', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + client_id: env.OSU_CLIENT_ID!, + client_secret: env.OSU_CLIENT_SECRET!, + grant_type: 'refresh_token', + refresh_token: acc.refreshToken, + scope: 'identify public' + }) + }); + + if (!res.ok) return null; + + const data = await res.json(); + await db + .update(account) + .set({ + accessToken: data.access_token, + refreshToken: data.refresh_token, + accessTokenExpiresAt: new Date(Date.now() + data.expires_in * 1000) + }) + .where(eq(account.id, acc.id)); + + return data.access_token; + } + + return acc.accessToken; +} + +async function osuFetch(path: string, token?: string) { + const t = token ?? (await getClientToken()); + const res = await fetch(`https://osu.ppy.sh/api/v2${path}`, { + headers: { Authorization: `Bearer ${t}` } + }); + + if (!res.ok) { + throw new Error(`osu! API ${path}: ${res.status} ${res.statusText}`); + } + + return res.json(); +} + +// ── Public API (with caching) ─────────────────────────────────────────── + +export async function getBeatmap(beatmapId: string | number) { + const cacheKey = `beatmap:${beatmapId}`; + const cached = getCached(cacheKey); + if (cached) return cached; + + const data = await osuFetch(`/beatmaps/${beatmapId}`); + setCache(cacheKey, data); + return data; +} + +export async function getBeatmapset(beatmapsetId: string | number) { + const cacheKey = `beatmapset:${beatmapsetId}`; + const cached = getCached(cacheKey); + if (cached) return cached; + + const data = await osuFetch(`/beatmapsets/${beatmapsetId}`); + setCache(cacheKey, data); + return data; +} + +export async function getUser(userId: string | number) { + const cacheKey = `osu_user:${userId}`; + const cached = getCached(cacheKey); + if (cached) return cached; + + const data = await osuFetch(`/users/${userId}`); + setCache(cacheKey, data, 5 * 60 * 1000); // 5 min for user data + return data; +} + +export async function getUserScores( + userId: string | number, + type: 'best' | 'recent' | 'firsts' = 'best', + limit = 10 +) { + return osuFetch(`/users/${userId}/scores/${type}?limit=${limit}`); +} + +export { getUserToken, getClientToken }; diff --git a/src/lib/server/osu/cache.ts b/src/lib/server/osu/cache.ts new file mode 100644 index 0000000..cc0ed1d --- /dev/null +++ b/src/lib/server/osu/cache.ts @@ -0,0 +1,42 @@ +// src/lib/server/osu/cache.ts +// +// Simple in-memory cache for osu! beatmap metadata. +// TTL: 10 minutes. Max entries: 500. +// Avoids hammering the osu! API on every page load. + +interface CacheEntry { + data: T; + expiresAt: number; +} + +const CACHE_TTL = 10 * 60 * 1000; // 10 minutes +const MAX_ENTRIES = 500; + +const cache = new Map>(); + +export function getCached(key: string): T | null { + const entry = cache.get(key); + if (!entry) return null; + if (Date.now() > entry.expiresAt) { + cache.delete(key); + return null; + } + return entry.data as T; +} + +export function setCache(key: string, data: T, ttl = CACHE_TTL): void { + // Evict oldest entries if at capacity + if (cache.size >= MAX_ENTRIES) { + const firstKey = cache.keys().next().value; + if (firstKey) cache.delete(firstKey); + } + cache.set(key, { data, expiresAt: Date.now() + ttl }); +} + +export function invalidateCache(key: string): void { + cache.delete(key); +} + +export function clearCache(): void { + cache.clear(); +} diff --git a/src/lib/server/permissions.ts b/src/lib/server/permissions.ts new file mode 100644 index 0000000..08faebb --- /dev/null +++ b/src/lib/server/permissions.ts @@ -0,0 +1,56 @@ +// src/lib/server/permissions.ts +import { error } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { user } from '$lib/server/db/auth.schema'; +import { eq } from 'drizzle-orm'; + +export type UserRole = 'player' | 'referee' | 'admin'; + +const ROLE_HIERARCHY: Record = { + player: 0, + referee: 1, + admin: 2 +}; + +/** + * Check if a role meets the minimum required level. + */ +export function hasRole(userRole: string | undefined, minRole: UserRole): boolean { + const level = ROLE_HIERARCHY[userRole as UserRole] ?? 0; + const required = ROLE_HIERARCHY[minRole]; + return level >= required; +} + +/** + * Throw 403 if user doesn't have the required role. + * Use in +page.server.ts load/actions. + */ +export function requireRole( + locals: App.Locals, + minRole: UserRole, + message = 'Insufficient permissions' +) { + if (!locals.user) { + error(401, 'Not logged in'); + } + if (!hasRole(locals.user.role, minRole)) { + error(403, message); + } +} + +/** + * Promote/demote a user's global role. Only admins can do this. + */ +export async function setUserRole(userId: string, role: UserRole) { + await db.update(user).set({ role }).where(eq(user.id, userId)); +} + +/** + * Get a user's role from the database (for fresh reads). + */ +export async function getUserRole(userId: string): Promise { + const u = await db.query.user.findFirst({ + where: eq(user.id, userId) + }); + return (u?.role as UserRole) ?? 'player'; +} diff --git a/src/routes/(app)/+layout.server.ts b/src/routes/(app)/+layout.server.ts new file mode 100644 index 0000000..68c8dd7 --- /dev/null +++ b/src/routes/(app)/+layout.server.ts @@ -0,0 +1,16 @@ +// src/routes/(app)/+layout.server.ts +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = async ({ locals }) => { + return { + user: locals.user + ? { + id: locals.user.id, + name: locals.user.name, + email: locals.user.email, + image: locals.user.image, + role: locals.user.role ?? 'player' + } + : null + }; +}; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte new file mode 100644 index 0000000..68f820d --- /dev/null +++ b/src/routes/(app)/+layout.svelte @@ -0,0 +1,48 @@ + + +{#if user} + +
+ +
+
+
+ {@render children()} +
+
+
+{:else} + +
+
+ +
+ V +
+ Vash Esports +
+ + Sign in with osu! + +
+
+ {@render children()} +
+
+{/if} diff --git a/src/routes/(app)/+page.server.ts b/src/routes/(app)/+page.server.ts new file mode 100644 index 0000000..9edf649 --- /dev/null +++ b/src/routes/(app)/+page.server.ts @@ -0,0 +1,49 @@ +// src/routes/(app)/+page.server.ts +import { db } from '$lib/server/db'; +import { match, team, mappool } from '$lib/server/db/schema'; +import { eq, desc, count, inArray } from 'drizzle-orm'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ locals }) => { + // ── NOT LOGGED IN → landing page (no redirect loop!) ── + if (!locals.user) { + return { + authenticated: false as const, + recentMatches: [], + liveMatches: [], + stats: { matches: 0, finished: 0, teams: 0, mappools: 0 } + }; + } + + // ── LOGGED IN → dashboard data ── + const recentMatches = await db.query.match.findMany({ + with: { participants: { with: { team: true } } }, + orderBy: desc(match.createdAt), + limit: 5 + }); + + const liveMatches = await db.query.match.findMany({ + with: { participants: { with: { team: true } } }, + where: inArray(match.state, ['LOBBY', 'ROLLING', 'PICKING', 'PLAYING']) + }); + + const [matchCount] = await db.select({ count: count() }).from(match); + const [teamCount] = await db.select({ count: count() }).from(team); + const [poolCount] = await db.select({ count: count() }).from(mappool); + const [finishedCount] = await db + .select({ count: count() }) + .from(match) + .where(eq(match.state, 'FINISHED')); + + return { + authenticated: true as const, + recentMatches, + liveMatches, + stats: { + matches: matchCount.count, + finished: finishedCount.count, + teams: teamCount.count, + mappools: poolCount.count + } + }; +}; diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte new file mode 100644 index 0000000..12fc307 --- /dev/null +++ b/src/routes/(app)/+page.svelte @@ -0,0 +1,312 @@ + + + +{#if !data.user} +
+
+ V +
+

+ Welcome to Vash Esports +

+

+ Automated tournament match management for osu! — lobbies, mappools, picks, bans, and + ELO tracking all in one place. +

+ + Sign in with osu! + +

+ Uses your osu! account. No extra registration needed. +

+
+ + +{:else} +
+
+

+ Welcome back, {data.user.name} +

+

Automated esports match management for osu!

+
+ + +
+
+

{data.stats.matches}

+

Total Matches

+
+
+

{data.stats.finished}

+

Completed

+
+
+

{data.stats.teams}

+

Teams

+
+
+

{data.stats.mappools}

+

Mappools

+
+
+ + + {#if data.liveMatches.length > 0} +
+
+
+
+
+
+

Live Now

+
+
+ {#each data.liveMatches as m} + {@const p1 = m.participants[0]} + {@const p2 = m.participants[1]} + {@const sc = stateConfig[m.state]} + +
+ {#if p1?.team.avatarUrl} + + {/if} + {p1?.team.name ?? '?'} +
+
+ {p1?.score ?? 0} + vs + {p2?.score ?? 0} +
+
+ {p2?.team.name ?? '?'} + {#if p2?.team.avatarUrl} + + {/if} +
+ + {#if sc?.dot} + + {/if} + {sc?.label} + +
+ {/each} +
+
+ {/if} + + +
+
+
+

Ranked Queue

+

Find a match at your skill level

+
+ {#if queueStatus?.inQueue} +
+ + + In queue ({queueStatus.queueSize} searching) + + +
+ {:else} + + {/if} +
+
+ + +
+{/if} diff --git a/src/routes/(app)/admin/+page.server.ts b/src/routes/(app)/admin/+page.server.ts new file mode 100644 index 0000000..c501d54 --- /dev/null +++ b/src/routes/(app)/admin/+page.server.ts @@ -0,0 +1,61 @@ +// src/routes/(app)/admin/+page.server.ts +import { db } from '$lib/server/db'; +import { user } from '$lib/server/db/auth.schema'; +import { playerRating } from '$lib/server/db/schema'; +import { eq, desc } from 'drizzle-orm'; +import { redirect, error } from '@sveltejs/kit'; +import { requireRole, setUserRole } from '$lib/server/permissions'; +import type { PageServerLoad, Actions } from './$types'; + +export const load: PageServerLoad = async ({ locals }) => { + requireRole(locals, 'admin'); + + const users = await db.query.user.findMany({ + orderBy: desc(user.createdAt) + }); + + // Fetch ratings for all users + const usersWithRatings = await Promise.all( + users.map(async (u) => { + const rating = await db.query.playerRating.findFirst({ + where: eq(playerRating.userId, u.id) + }); + return { + id: u.id, + name: u.name, + email: u.email, + image: u.image, + role: u.role ?? 'player', + createdAt: u.createdAt, + elo: rating?.elo ?? 1000, + wins: rating?.wins ?? 0, + losses: rating?.losses ?? 0 + }; + }) + ); + + return { users: usersWithRatings }; +}; + +export const actions: Actions = { + setRole: async ({ request, locals }) => { + requireRole(locals, 'admin'); + + const form = await request.formData(); + const userId = form.get('userId')?.toString(); + const role = form.get('role')?.toString(); + + if (!userId || !role) return { error: 'User ID and role are required' }; + if (!['player', 'referee', 'admin'].includes(role)) { + return { error: 'Invalid role' }; + } + + // Prevent demoting yourself + if (userId === locals.user!.id && role !== 'admin') { + return { error: "You can't demote yourself" }; + } + + await setUserRole(userId, role as any); + return { success: true }; + } +}; diff --git a/src/routes/(app)/admin/+page.svelte b/src/routes/(app)/admin/+page.svelte new file mode 100644 index 0000000..1658847 --- /dev/null +++ b/src/routes/(app)/admin/+page.svelte @@ -0,0 +1,103 @@ + + +
+
+

Admin Panel

+

+ {data.users.length} registered user{data.users.length !== 1 ? 's' : ''} +

+
+ + {#if actionError} +

{actionError}

+ {/if} + + +
+
+ Avatar + User + ELO + W/L + Role +
+ + {#each data.users as u} + {@const badge = roleBadge[u.role] ?? roleBadge.player} +
+ + {#if u.image} + + {:else} +
+ {u.name.charAt(0).toUpperCase()} +
+ {/if} + + +
+

{u.name}

+

{u.email}

+
+ + + {u.elo} + + + + {u.wins}W / + {u.losses}L + + + +
{ + actionError = ''; + return async ({ result }) => { + if ( + result.type === 'failure' || + (result.type === 'success' && (result.data as any)?.error) + ) { + actionError = (result.data as any)?.error ?? 'Failed to update role'; + } + }; + }} + class="flex items-center gap-2" + > + + +
+
+ {/each} +
+
diff --git a/src/routes/(app)/layout.css b/src/routes/(app)/layout.css new file mode 100644 index 0000000..107af4c --- /dev/null +++ b/src/routes/(app)/layout.css @@ -0,0 +1,43 @@ +@import 'tailwindcss'; +@plugin '@tailwindcss/typography'; + +@theme { + --color-surface-900: #0a0a0f; + --color-surface-800: #12121a; + --color-surface-700: #1a1a25; + --color-surface-600: #24243a; + --color-accent: #ff66ab; + --color-accent-hover: #ff85be; + --color-accent-dim: #ff66ab33; + --color-text-primary: #eeeef0; + --color-text-secondary: #8888a0; + --color-border: #2a2a3d; + --font-display: 'Geist', system-ui, sans-serif; + --font-body: 'Geist', system-ui, sans-serif; + --font-mono: 'Geist Mono', ui-monospace, monospace; +} + +html { + background-color: var(--color-surface-900); + color: var(--color-text-primary); + font-family: var(--font-body); +} + +body { + min-height: 100dvh; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 6px; +} +::-webkit-scrollbar-track { + background: var(--color-surface-900); +} +::-webkit-scrollbar-thumb { + background: var(--color-surface-600); + border-radius: 3px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--color-accent); +} diff --git a/src/routes/(app)/mappools/+page.server.ts b/src/routes/(app)/mappools/+page.server.ts new file mode 100644 index 0000000..0120554 --- /dev/null +++ b/src/routes/(app)/mappools/+page.server.ts @@ -0,0 +1,32 @@ +import { db } from '$lib/server/db'; +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; +import { mappool } from '$lib/server/db/schema'; + +export const load: PageServerLoad = async ({ locals }) => { + if (!locals.user) redirect(302, '/'); + + const mappools = await db.query.mappool.findMany({ + with: { slots: true }, + orderBy: (m, { desc }) => [desc(m.createdAt)] + }); + + return { mappools }; +}; + +export const actions: Actions = { + create: async ({ request, locals }) => { + if (!locals.user) redirect(302, '/'); + + const form = await request.formData(); + const name = form.get('name')?.toString()?.trim(); + if (!name) return { error: 'Name is required' }; + + const [created] = await db + .insert(mappool) + .values({ name, createdBy: locals.user.id }) + .returning(); + + redirect(303, `/mappools/${created.id}`); + } +}; diff --git a/src/routes/(app)/mappools/+page.svelte b/src/routes/(app)/mappools/+page.svelte new file mode 100644 index 0000000..d54b111 --- /dev/null +++ b/src/routes/(app)/mappools/+page.svelte @@ -0,0 +1,66 @@ + + +
+
+
+

Mappools

+

Create and manage your beatmap pools

+
+ +
+ + {#if creating} +
+ + +
+ {/if} + +
+ {#each data.mappools as pool} + +
+

{pool.name}

+

+ {pool.slots.length} map{pool.slots.length !== 1 ? 's' : ''} + · Created {new Date(pool.createdAt).toLocaleDateString()} +

+
+ → +
+ {:else} +
+

No mappools yet. Create one to get started.

+
+ {/each} +
+
diff --git a/src/routes/(app)/mappools/[id]/+page.server.ts b/src/routes/(app)/mappools/[id]/+page.server.ts new file mode 100644 index 0000000..5eb2b71 --- /dev/null +++ b/src/routes/(app)/mappools/[id]/+page.server.ts @@ -0,0 +1,118 @@ +import { db } from '$lib/server/db'; +import { mappool, mappoolSlot } from '$lib/server/db/schema'; +import { eq, and } from 'drizzle-orm'; +import { error, redirect } from '@sveltejs/kit'; +import { getBeatmap } from '$lib/server/osu/api'; +import type { PageServerLoad, Actions } from './$types'; + +export const load: PageServerLoad = async ({ params, locals }) => { + if (!locals.user) redirect(302, '/'); + + const pool = await db.query.mappool.findFirst({ + where: eq(mappool.id, params.id), + with: { + slots: { + orderBy: (s, { asc }) => [asc(s.category), asc(s.orderInCategory)] + } + } + }); + + if (!pool) error(404, 'Mappool not found'); + + // Fetch beatmap metadata for all slots + const slotsWithBeatmaps = await Promise.all( + pool.slots.map(async (slot) => { + try { + const beatmap = await getBeatmap(slot.beatmapId); + return { + ...slot, + beatmap: { + title: beatmap.beatmapset.title, + artist: beatmap.beatmapset.artist, + version: beatmap.version, + starRating: beatmap.difficulty_rating, + bpm: beatmap.bpm, + totalLength: beatmap.total_length, + coverUrl: beatmap.beatmapset.covers['card@2x'], + url: beatmap.url + } + }; + } catch { + return { + ...slot, + beatmap: null + }; + } + }) + ); + + return { + pool: { ...pool, slots: slotsWithBeatmaps } + }; +}; + +export const actions: Actions = { + addSlot: async ({ params, request, locals }) => { + if (!locals.user) redirect(302, '/'); + + const form = await request.formData(); + let beatmapId = form.get('beatmapId')?.toString()?.trim(); + const category = form.get('category')?.toString()?.trim()?.toUpperCase(); + + if (!beatmapId || !category) { + return { error: 'Beatmap ID and category are required' }; + } + + const urlMatch = beatmapId.match(/beatmaps\/(\d+)/); + const setMatch = beatmapId.match(/beatmapsets\/\d+#\w+\/(\d+)/); + if (urlMatch) beatmapId = urlMatch[1]; + else if (setMatch) beatmapId = setMatch[1]; + + if (!/^\d+$/.test(beatmapId)) { + return { error: 'Invalid beatmap ID' }; + } + + let beatmap; + try { + beatmap = await getBeatmap(beatmapId); + } catch { + return { error: 'Beatmap not found on osu!' }; + } + + const existing = await db.query.mappoolSlot.findMany({ + where: and( + eq(mappoolSlot.mappoolId, params.id), + eq(mappoolSlot.category, category) + ) + }); + + await db.insert(mappoolSlot).values({ + mappoolId: params.id, + beatmapId, + category, + orderInCategory: existing.length + 1, + starRating: beatmap.difficulty_rating, + mods: category === 'NM' || category === 'TB' || category === 'FM' ? [] : [category] + }); + + return { success: true }; +}, + + removeSlot: async ({ request, locals }) => { + if (!locals.user) redirect(302, '/'); + + const form = await request.formData(); + const slotId = form.get('slotId')?.toString(); + if (!slotId) return { error: 'Missing slot ID' }; + + await db.delete(mappoolSlot).where(eq(mappoolSlot.id, slotId)); + return { success: true }; + }, + + deletePool: async ({ params, locals }) => { + if (!locals.user) redirect(302, '/'); + + await db.delete(mappool).where(eq(mappool.id, params.id)); + redirect(303, '/mappools'); + } +}; diff --git a/src/routes/(app)/mappools/[id]/+page.svelte b/src/routes/(app)/mappools/[id]/+page.svelte new file mode 100644 index 0000000..9616476 --- /dev/null +++ b/src/routes/(app)/mappools/[id]/+page.svelte @@ -0,0 +1,205 @@ + + +
+ +
+
+ ← +
+

{data.pool.name}

+

+ {data.pool.slots.length} map{data.pool.slots.length !== 1 ? 's' : ''} +

+
+
+ +
+ +
+
+ + +
{ + searching = true; + addError = ''; + return async ({ result, update }) => { + searching = false; + if (result.type === 'success') { + beatmapInput = ''; + await update(); + } else if (result.type === 'failure') { + addError = (result.data as { error?: string })?.error ?? 'Failed to add map'; } + }; + }} + class="mt-6 rounded-lg border border-border bg-surface-800 p-4" + > +

Add Map

+
+ + + + + +
+ + {#if addError} +

{addError}

+ {/if} +
+ + +
+ {#each Object.entries(grouped()) as [category, slots]} +
+
+ + {category} + + {slots.length} map{slots.length !== 1 ? 's' : ''} +
+ +
+ {#each slots as slot, i} +
+ + {#if slot.beatmap?.coverUrl} + + {:else} +
+ No cover +
+ {/if} + + +
+ {#if slot.beatmap} + + {slot.beatmap.artist} - {slot.beatmap.title} + +
+ [{slot.beatmap.version}] + ★ {slot.beatmap.starRating.toFixed(2)} + {slot.beatmap.bpm} BPM + {formatLength(slot.beatmap.totalLength)} +
+ {:else} +

Beatmap #{slot.beatmapId}

+ {/if} +
+ + + + {category}{i + 1} + + + +
+ + +
+
+ {/each} +
+
+ {:else} +
+

No maps yet. Add your first map above.

+
+ {/each} +
+
diff --git a/src/routes/(app)/matches/+page.server.ts b/src/routes/(app)/matches/+page.server.ts new file mode 100644 index 0000000..07f899d --- /dev/null +++ b/src/routes/(app)/matches/+page.server.ts @@ -0,0 +1,71 @@ +import { db } from '$lib/server/db'; +import { match } from '$lib/server/db/schema'; +import { redirect } from '@sveltejs/kit'; +import { desc } from 'drizzle-orm'; +import { createMatch } from '$lib/server/match/engine'; +import { initMatchLobby } from '$lib/server/match/orchestrator'; +import type { PageServerLoad, Actions } from './$types'; + +export const load: PageServerLoad = async ({ locals }) => { + if (!locals.user) redirect(302, '/'); + + const matches = await db.query.match.findMany({ + with: { participants: { with: { team: true } } }, + orderBy: desc(match.createdAt), + limit: 50 + }); + + const teams = await db.query.team.findMany({ + orderBy: (t, { asc }) => [asc(t.name)] + }); + + const mappools = await db.query.mappool.findMany({ + with: { slots: true }, + orderBy: (m, { desc }) => [desc(m.createdAt)] + }); + + return { matches, teams, mappools }; +}; + +export const actions: Actions = { + createMatch: async ({ request, locals }) => { + if (!locals.user) redirect(302, '/'); + + const form = await request.formData(); + const name = form.get('name')?.toString()?.trim() || 'Custom Match'; + const team1Id = form.get('team1')?.toString(); + const team2Id = form.get('team2')?.toString(); + const mappoolId = form.get('mappool')?.toString(); + const bestOf = parseInt(form.get('bestOf')?.toString() ?? '7'); + + console.log('[Matches] CREATE:', { name, team1Id, team2Id, mappoolId, bestOf }); + + if (!team1Id || !team2Id || !mappoolId) { + return { error: 'All fields are required' }; + } + + // Same team is allowed for testing/dev + + let result; + try { + result = await createMatch({ + name, + config: { bestOf, teamSize: 1, scoringType: 'score' }, + mappoolId, + teams: [team1Id, team2Id], + createdBy: locals.user.id + }); + console.log('[Matches] Created:', result.id); + } catch (e: any) { + console.error('[Matches] Error:', e); + return { error: e.message }; + } + + // Create IRC lobby in background (don't block redirect) + initMatchLobby(result.id).catch((err) => { + console.error('[Matches] IRC lobby failed:', err.message); + }); + + redirect(303, `/matches/${result.id}`); + } +}; diff --git a/src/routes/(app)/matches/+page.svelte b/src/routes/(app)/matches/+page.svelte new file mode 100644 index 0000000..f7212ad --- /dev/null +++ b/src/routes/(app)/matches/+page.svelte @@ -0,0 +1,275 @@ + + +
+ +
+
+

Matches

+

+ {data.matches.length} match{data.matches.length !== 1 ? 'es' : ''} + {#if liveMatches.length > 0} + · {liveMatches.length} live + {/if} +

+
+ +
+ + + {#if showCreate} +
{ + createError = ''; + return async ({ result, update }) => { + if (result.type === 'failure' || (result.type === 'success' && (result.data as any)?.error)) { + createError = (result.data as any)?.error ?? 'Failed to create match'; + } else { + await update(); + } + }; + }} + class="mt-4 rounded-lg border border-accent/20 bg-surface-800 p-5" + > +

Create Match

+ + {#if createError} +

{createError}

+ {/if} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +
+
+ {/if} + + +{#if liveMatches.length > 0} + + {/if} + + + +
diff --git a/src/routes/(app)/matches/[id]/+page.server.ts b/src/routes/(app)/matches/[id]/+page.server.ts new file mode 100644 index 0000000..6abbb7f --- /dev/null +++ b/src/routes/(app)/matches/[id]/+page.server.ts @@ -0,0 +1,162 @@ +import { user } from '$lib/server/db/schema'; +import { db } from '$lib/server/db'; +import { getMatchFull, submitRoll, pickMap, cancelMatch } from '$lib/server/match/engine'; +import { playPickedMap, closeLobby, forceStartGame } from '$lib/server/match/orchestrator'; +import { getLobby } from '$lib/server/bancho/client'; +import { error, redirect } from '@sveltejs/kit'; +import { getBeatmap } from '$lib/server/osu/api'; +import type { PageServerLoad, Actions } from './$types'; +import { eq } from 'drizzle-orm'; + + +export const load: PageServerLoad = async ({ params, locals }) => { + if (!locals.user) redirect(302, '/'); + + let m; + try { + m = await getMatchFull(params.id); + } catch { + error(404, 'Match not found'); + } + + // Build beatmap metadata cache for all slots in the mappool + const beatmapCache: Record = {}; + if (m.mappool?.slots) { + await Promise.allSettled( + m.mappool.slots.map(async (slot) => { + try { + const bm = await getBeatmap(slot.beatmapId); + beatmapCache[slot.beatmapId] = { + title: bm.beatmapset.title, + artist: bm.beatmapset.artist, + version: bm.version, + starRating: bm.difficulty_rating, + bpm: bm.bpm, + totalLength: bm.total_length, + coverUrl: bm.beatmapset.covers['card@2x'], + listCoverUrl: bm.beatmapset.covers['list@2x'] + }; + } catch { /* skip */ } + }) + ); + } + + return { match: m, beatmapCache, userId: locals.user.id }; +}; + +export const actions: Actions = { + reinvite: async ({ params, locals }) => { + if (!locals.user) redirect(302, '/'); + + const lobby = getLobby(params.id); + if (!lobby) return { error: 'No active IRC lobby for this match' }; + + const m = await getMatchFull(params.id); + + const invited: string[] = []; + for (const p of m.participants) { + for (const pl of p.players) { + const u = await db.query.user.findFirst({ where: eq(user.id, pl.userId) }); + if (u?.name) { + await lobby.invite(u.name); + invited.push(u.name); + } + } + } + + return { reinvited: invited }; + }, + + roll: async ({ params, locals }) => { + if (!locals.user) redirect(302, '/'); + + const m = await getMatchFull(params.id); + + // Find the participant linked to the current user + let myParticipant = m.participants.find((p) => + p.players.some((pl) => pl.userId === locals.user!.id) + ); + + if (!myParticipant) { + return { error: 'You are not in this match' }; + } + + // For same-team testing: if user is on both sides, pick the one that hasn't rolled yet + if (myParticipant.rollValue !== null) { + const unrolled = m.participants.find((p) => p.rollValue === null); + if (unrolled) { + myParticipant = unrolled; + } else { + return { error: 'All participants have already rolled' }; + } + } + + const value = Math.floor(Math.random() * 100) + 1; + await submitRoll(params.id, myParticipant.id, value); + + // Announce in IRC lobby + const lobby = getLobby(params.id); + if (lobby) { + lobby.chat(`${myParticipant.team?.name ?? 'Player'} rolled ${value} (via web)`).catch(() => {}); + + // Check if all rolled → announce pick order + const updated = await getMatchFull(params.id); + if (updated.state === 'PICKING') { + const sorted = [...updated.participants].sort( + (a: any, b: any) => (a.pickOrder ?? 99) - (b.pickOrder ?? 99) + ); + lobby.chat( + `Rolls complete! ${sorted[0]?.team.name} picks first. Use !pick (e.g. !pick NM1) or pick in web UI.` + ).catch(() => {}); + } + } + + return { rolled: value, participantId: myParticipant.id }; + }, + + pick: async ({ params, request, locals }) => { + if (!locals.user) redirect(302, '/'); + + const form = await request.formData(); + const slotId = form.get('slotId')?.toString(); + if (!slotId) return { error: 'No map selected' }; + + const m = await getMatchFull(params.id); + + // Find the expected picker based on pick order and game count + const sorted = [...m.participants].sort( + (a, b) => (a.pickOrder ?? 99) - (b.pickOrder ?? 99) + ); + const expectedIdx = m.games.length % sorted.length; + const expectedPicker = sorted[expectedIdx]; + + if (!expectedPicker) return { error: 'Cannot determine picker' }; + + try { + const game = await pickMap(params.id, expectedPicker.id, slotId); + + // Set map + mods in IRC lobby and start game (background) + playPickedMap(params.id, game.id).catch((err) => + console.error('[Match] IRC play failed:', err.message) + ); + + return { picked: true, gameId: game.id }; + } catch (e: any) { + return { error: e.message }; + } + }, + + forceStart: async ({ params, locals }) => { + if (!locals.user) redirect(302, '/'); + await forceStartGame(params.id); + return { started: true }; + }, + + cancel: async ({ params, locals }) => { + if (!locals.user) redirect(302, '/'); + + await cancelMatch(params.id); + await closeLobby(params.id); + redirect(303, '/matches'); + } +}; diff --git a/src/routes/(app)/matches/[id]/+page.svelte b/src/routes/(app)/matches/[id]/+page.svelte new file mode 100644 index 0000000..28057a6 --- /dev/null +++ b/src/routes/(app)/matches/[id]/+page.svelte @@ -0,0 +1,410 @@ + + +
+ +
+ ← +
+

{m.name || 'Match'}

+

+ Best of {config.bestOf} · First to {winsNeeded} + {#if m.osuLobbyId} + · osu! mp/{m.osuLobbyId} + {/if} +

+
+ {stateLabel(m.state)} + + {#if !['FINISHED', 'CANCELLED'].includes(m.state)} +
+ +
+ {/if} +
+ + + {#if p1 && p2} +
+ +
+ {#if p1.team.avatarUrl} + + {/if} +
+

{p1.team.name}

+ {#if p1.rollValue != null} +

Roll: {p1.rollValue}{p1.pickOrder === 1 ? ' ★' : ''}

+ {/if} +
+ {p1.score} +
+ + vs + + +
+ {p2.score} +
+

{p2.team.name}

+ {#if p2.rollValue != null} +

{p2.pickOrder === 1 ? '★ ' : ''}Roll: {p2.rollValue}

+ {/if} +
+ {#if p2.team.avatarUrl} + + {/if} +
+
+ {/if} + + + {#if ['LOBBY', 'ROLLING', 'PICKING', 'PLAYING'].includes(m.state)} +
+
{ + return async ({ result, update }) => { + if (result.type === 'success' && (result.data as any)?.reinvited) { + // Brief visual feedback — just reload + await update(); + } + }; + }}> + +
+
+ {/if} + + + {#if m.state === 'ROLLING'} +
+
+

🎲 Roll Phase

+

Highest roll picks first

+ +
+ {#each m.participants as p} +
+

{p.team.name}

+ {#if p.rollValue != null} +

{p.rollValue}

+ {:else} +

—

+ {/if} +
+ {/each} +
+ + {#if canRoll} +
{ + rolling = true; + return async ({ update }) => { rolling = false; await update(); }; + }}> + +
+

or type !roll in osu! chat

+ {:else} +

Waiting for all rolls...

+ {/if} +
+
+ {/if} + + + {#if m.state === 'PICKING'} +
+
+
+

🎯 Pick a Map

+

+ {expectedPicker?.team.name}'s turn to pick +

+

+ or type !pick NM1 in osu! chat +

+
+ + {#if pickError} +

{pickError}

+ {/if} + +
+ {#each Object.entries(groupedSlots()) as [category, slots]} +
+ + {category} + +
+ {#each slots as slot, i} + {@const bm = data.beatmapCache[slot.beatmapId]} + {@const isPlayed = playedSlotIds.has(slot.id)} + +
{ + picking = true; + pickError = ''; + return async ({ result, update }) => { + picking = false; + if (result.type === 'failure' || (result.type === 'success' && (result.data as any)?.error)) { + pickError = (result.data as any)?.error ?? 'Pick failed'; + } else { + await update(); + } + }; + }}> + + +
+ {/each} +
+
+ {/each} +
+
+
+ {/if} + + + {#if m.state === 'PLAYING'} + {@const currentGame = m.games[m.games.length - 1]} + {@const bm = currentGame ? data.beatmapCache[currentGame.slot?.beatmapId] : null} +
+
+
+
+
+
+ Now Playing +
+ {#if bm} +

{bm.artist} - {bm.title} [{bm.version}]

+

★{bm.starRating.toFixed(1)} · {bm.bpm}bpm · {formatLength(bm.totalLength)}

+ {/if} +

+ Waiting for players to ready up & play in osu!... +

+ + +
{ + forceStarting = true; + return async ({ update }) => { forceStarting = false; await update(); }; + }}> + +
+
+ {/if} + + + {#if m.state === 'FINISHED'} + {@const winner = m.participants.find((p: any) => p.teamId === m.winnerId)} +
+
+
🏆
+

+ {winner?.team.name} wins! +

+

+ {p1?.score} – {p2?.score} +

+ + +
+ {#each m.games as game} + {@const bm = data.beatmapCache[game.slot?.beatmapId]} + {@const winnerP = m.participants.find((p: any) => p.id === game.winnerParticipantId)} +
+ {game.slot?.category}{game.slot?.orderInCategory} + {bm ? `${bm.artist} - ${bm.title}` : `#${game.slot?.beatmapId}`} + {winnerP?.team.name} +
+ {/each} +
+ + + Back to Dashboard + +
+
+ {/if} + + + {#if m.games.length > 0} +
+

Games Played

+
+ {#each m.games as game} + {@const bm = data.beatmapCache[game.slot?.beatmapId]} + {@const pickerTeam = m.participants.find((p: any) => p.id === game.pickedByParticipantId)?.team} + {@const winnerTeam = m.participants.find((p: any) => p.id === game.winnerParticipantId)?.team} + +
+ {game.gameNumber} + {#if bm?.listCoverUrl} + + {/if} +
+ {#if bm} +

{bm.artist} - {bm.title} [{bm.version}]

+ {/if} +
+ {game.slot?.category}{game.slot?.orderInCategory} + {#if pickerTeam}Picked by {pickerTeam.name}{/if} +
+
+ + {#if game.state === 'FINISHED' && p1 && p2} + {@const p1Score = game.scores.filter((s: any) => s.player?.participantId === p1.id).reduce((sum: number, s: any) => sum + s.score, 0)} + {@const p2Score = game.scores.filter((s: any) => s.player?.participantId === p2.id).reduce((sum: number, s: any) => sum + s.score, 0)} +
+ {p1Score.toLocaleString()} + - + {p2Score.toLocaleString()} +
+ {:else if game.state === 'PLAYING'} + Live + {/if} + + {#if winnerTeam} + ✓ {winnerTeam.name} + {/if} +
+ {/each} +
+
+ {/if} + + + {#if m.state === 'CREATED'} +
+

Creating osu! lobby...

+
+ {/if} + + {#if m.state === 'LOBBY'} +
+

Lobby created. Waiting for players to join...

+ {#if m.osuLobbyId} +

+ osu! mp/{m.osuLobbyId} +

+ {/if} +
+ {/if} + + {#if m.state === 'CANCELLED'} +
+

This match was cancelled.

+
+ {/if} +
diff --git a/src/routes/(app)/settings/+page.server.ts b/src/routes/(app)/settings/+page.server.ts new file mode 100644 index 0000000..d3788ca --- /dev/null +++ b/src/routes/(app)/settings/+page.server.ts @@ -0,0 +1,46 @@ +// src/routes/(app)/settings/+page.server.ts +import { redirect } from '@sveltejs/kit'; +import { auth } from '$lib/server/auth'; +import { db } from '$lib/server/db'; +import { playerRating } from '$lib/server/db/schema'; +import { account } from '$lib/server/db/auth.schema'; +import { eq, and } from 'drizzle-orm'; +import type { PageServerLoad, Actions } from './$types'; + +export const load: PageServerLoad = async ({ locals }) => { + if (!locals.user) redirect(302, '/'); + + // Get player rating + const rating = await db.query.playerRating.findFirst({ + where: eq(playerRating.userId, locals.user.id) + }); + + // Get linked osu! account info + const osuAccount = await db.query.account.findFirst({ + where: and(eq(account.userId, locals.user.id), eq(account.providerId, 'osu')) + }); + + return { + profile: { + id: locals.user.id, + name: locals.user.name, + email: locals.user.email, + image: locals.user.image, + role: locals.user.role ?? 'player', + createdAt: locals.user.createdAt + }, + rating: rating + ? { elo: rating.elo, wins: rating.wins, losses: rating.losses } + : { elo: 1000, wins: 0, losses: 0 }, + hasOsuLinked: !!osuAccount + }; +}; + +export const actions: Actions = { + logout: async (event) => { + await auth.api.signOut({ + headers: event.request.headers + }); + redirect(302, '/'); + } +}; diff --git a/src/routes/(app)/settings/+page.svelte b/src/routes/(app)/settings/+page.svelte new file mode 100644 index 0000000..bd80675 --- /dev/null +++ b/src/routes/(app)/settings/+page.svelte @@ -0,0 +1,114 @@ + + +
+

Settings

+

Your profile and account settings

+ + +
+
+ {#if data.profile.image} + {data.profile.name} + {:else} +
+ {data.profile.name.charAt(0).toUpperCase()} +
+ {/if} +
+
+

{data.profile.name}

+ + {badge.label} + +
+

{data.profile.email}

+
+
+ + +
+

Connected Accounts

+
+ 🎮 +
+

osu!

+

+ {#if data.hasOsuLinked} + Connected as {data.profile.name} + {:else} + Not connected + {/if} +

+
+ {#if data.hasOsuLinked} + Linked + {:else} + + Connect + + {/if} +
+
+
+ + +
+

Ranked Stats

+
+
+

{data.rating.elo}

+

ELO Rating

+
+
+

{data.rating.wins}

+

Wins

+
+
+

{data.rating.losses}

+

Losses

+
+
+

{winRate}{winRate !== '—' ? '%' : ''}

+

Win Rate

+
+
+
+ + +
+

Account

+
+
+ +
+
+
+
diff --git a/src/routes/(app)/teams/+page.server.ts b/src/routes/(app)/teams/+page.server.ts new file mode 100644 index 0000000..b6a8030 --- /dev/null +++ b/src/routes/(app)/teams/+page.server.ts @@ -0,0 +1,120 @@ +import { db } from '$lib/server/db'; +import { team, teamMember, user } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; + +export const load: PageServerLoad = async ({ locals }) => { + if (!locals.user) redirect(302, '/'); + + const teams = await db.query.team.findMany({ + with: { + members: true + }, + orderBy: (t, { desc }) => [desc(t.createdAt)] + }); + + // Fetch member user details + const teamsWithUsers = await Promise.all( + teams.map(async (t) => { + const membersWithUsers = await Promise.all( + t.members.map(async (m) => { + const u = await db.query.user.findFirst({ + where: eq(user.id, m.userId) + }); + return { ...m, user: u ? { id: u.id, name: u.name, image: u.image } : null }; + }) + ); + return { ...t, members: membersWithUsers }; + }) + ); + + return { teams: teamsWithUsers, userId: locals.user.id }; +}; + +export const actions: Actions = { + create: async ({ request, locals }) => { + if (!locals.user) redirect(302, '/'); + + const form = await request.formData(); + const name = form.get('name')?.toString()?.trim(); + + if (!name) return { error: 'Team name is required' }; + + const [created] = await db + .insert(team) + .values({ + name, + ownerId: locals.user.id, + isPersonal: false + }) + .returning(); + + // Add creator as captain + await db.insert(teamMember).values({ + teamId: created.id, + userId: locals.user.id, + role: 'captain' + }); + + return { success: true }; + }, + + addMember: async ({ request, locals }) => { + if (!locals.user) redirect(302, '/'); + + const form = await request.formData(); + const teamId = form.get('teamId')?.toString(); + const username = form.get('username')?.toString()?.trim(); + + if (!teamId || !username) return { error: 'Team and username are required' }; + + // Find user by osu! username + const u = await db.query.user.findFirst({ + where: eq(user.name, username) + }); + + if (!u) return { error: `User "${username}" not found. They need to log in first.` }; + + // Check not already member + const existing = await db.query.teamMember.findFirst({ + where: (m, { and, eq }) => and(eq(m.teamId, teamId), eq(m.userId, u.id)) + }); + + if (existing) return { error: `${username} is already on this team` }; + + await db.insert(teamMember).values({ + teamId, + userId: u.id, + role: 'member' + }); + + return { success: true }; + }, + + removeMember: async ({ request, locals }) => { + if (!locals.user) redirect(302, '/'); + + const form = await request.formData(); + const memberId = form.get('memberId')?.toString(); + if (!memberId) return { error: 'Missing member ID' }; + + await db.delete(teamMember).where(eq(teamMember.id, memberId)); + return { success: true }; + }, + + deleteTeam: async ({ request, locals }) => { + if (!locals.user) redirect(302, '/'); + + const form = await request.formData(); + const teamId = form.get('teamId')?.toString(); + if (!teamId) return { error: 'Missing team ID' }; + + // Only allow owner to delete + const t = await db.query.team.findFirst({ where: eq(team.id, teamId) }); + if (!t || t.ownerId !== locals.user.id) return { error: 'Not authorized' }; + + await db.delete(team).where(eq(team.id, teamId)); + return { success: true }; + } +}; diff --git a/src/routes/(app)/teams/+page.svelte b/src/routes/(app)/teams/+page.svelte new file mode 100644 index 0000000..3c87bef --- /dev/null +++ b/src/routes/(app)/teams/+page.svelte @@ -0,0 +1,229 @@ + + +
+ +
+
+

Teams

+

+ {data.teams.length} team{data.teams.length !== 1 ? 's' : ''} +

+
+ +
+ + + {#if creating} +
{ + return async ({ result, update }) => { + if (result.type === 'success' && (result.data as any)?.success) { + creating = false; + await update(); + } + }; + }} + class="mt-4 flex gap-3 rounded-lg border border-border bg-surface-800 p-4" + > + + +
+ {/if} + + {#if actionError} +

{actionError}

+ {/if} + + + {#if myTeams.length > 0} +
+

My Teams

+
+ {#each myTeams as t} +
+ + + + + {#if expandedTeam === t.id} +
+ +
+ {#each t.members as member} +
+ {#if member.user?.image} + + {:else} +
+ {/if} + {member.user?.name ?? 'Unknown'} + {member.role} + {#if isOwner(t) && member.userId !== data.userId} +
{ + return async ({ update }) => { await update(); }; + }}> + + +
+ {/if} +
+ {/each} +
+ + + {#if isOwner(t) && !t.isPersonal} + {#if addingMemberTo === t.id} +
{ + actionError = ''; + return async ({ result, update }) => { + if (result.type === 'success' && (result.data as any)?.success) { + memberUsername = ''; + addingMemberTo = null; + await update(); + } else if (result.type === 'success' && (result.data as any)?.error) { + actionError = (result.data as any).error; + } + }; + }} + class="mt-3 flex gap-2" + > + + + + +
+ {:else} + + {/if} + {/if} + + + {#if isOwner(t) && !t.isPersonal} +
+ + +
+ {/if} +
+ {/if} +
+ {/each} +
+
+ {/if} + + + {#if otherTeams.length > 0} +
+

Other Teams

+
+ {#each otherTeams as t} +
+ {#if t.avatarUrl} + + {:else} +
+ {t.name.charAt(0).toUpperCase()} +
+ {/if} +
+ {t.name} +

{t.members.length} member{t.members.length !== 1 ? 's' : ''}

+
+
+ {/each} +
+
+ {/if} + + + {#if data.teams.length === 0} +
+

No teams yet. Your personal team is created when you sign in.

+
+ {/if} +
diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 0000000..b30a3e6 --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,29 @@ + + +
+
+

{page.status}

+

+ {#if page.status === 404} + Page not found + {:else if page.status === 403} + Access denied + {:else if page.status === 401} + Not logged in + {:else} + Something went wrong + {/if} +

+

+ {page.error?.message ?? 'An unexpected error occurred.'} +

+ + Back to Dashboard + +
+
diff --git a/src/routes/api/health/+server.ts b/src/routes/api/health/+server.ts new file mode 100644 index 0000000..de0e629 --- /dev/null +++ b/src/routes/api/health/+server.ts @@ -0,0 +1,27 @@ +// src/routes/api/health/+server.ts +import { json } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { sql } from 'drizzle-orm'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async () => { + try { + // Check DB connectivity + await db.execute(sql`SELECT 1`); + + return json({ + status: 'ok', + timestamp: new Date().toISOString(), + uptime: process.uptime() + }); + } catch (e: any) { + return json( + { + status: 'error', + error: 'Database connection failed', + timestamp: new Date().toISOString() + }, + { status: 503 } + ); + } +}; diff --git a/src/routes/api/mappools/+server.ts b/src/routes/api/mappools/+server.ts new file mode 100644 index 0000000..6e40215 --- /dev/null +++ b/src/routes/api/mappools/+server.ts @@ -0,0 +1,33 @@ +import { json, error } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { mappool } from '$lib/server/db/schema'; +import type { RequestHandler } from './$types'; + +// List all mappools +export const GET: RequestHandler = async ({ locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + const mappools = await db.query.mappool.findMany({ + orderBy: (m, { desc }) => [desc(m.createdAt)] + }); + + return json(mappools); +}; + +// Create a mappool +export const POST: RequestHandler = async ({ request, locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + const { name } = await request.json(); + if (!name?.trim()) error(400, 'Name is required'); + + const [created] = await db + .insert(mappool) + .values({ + name: name.trim(), + createdBy: locals.user.id + }) + .returning(); + + return json(created, { status: 201 }); +}; diff --git a/src/routes/api/mappools/[id]/+server.ts b/src/routes/api/mappools/[id]/+server.ts new file mode 100644 index 0000000..72fdcd0 --- /dev/null +++ b/src/routes/api/mappools/[id]/+server.ts @@ -0,0 +1,31 @@ +import { json, error } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { mappool, mappoolSlot } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import type { RequestHandler } from './$types'; + +// Get a mappool with its slots +export const GET: RequestHandler = async ({ params, locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + const pool = await db.query.mappool.findFirst({ + where: eq(mappool.id, params.id) + }); + + if (!pool) error(404, 'Mappool not found'); + + const slots = await db.query.mappoolSlot.findMany({ + where: eq(mappoolSlot.mappoolId, params.id), + orderBy: (s, { asc }) => [asc(s.category), asc(s.orderInCategory)] + }); + + return json({ ...pool, slots }); +}; + +// Delete a mappool +export const DELETE: RequestHandler = async ({ params, locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + await db.delete(mappool).where(eq(mappool.id, params.id)); + return json({ ok: true }); +}; diff --git a/src/routes/api/mappools/[id]/slots/+server.ts b/src/routes/api/mappools/[id]/slots/+server.ts new file mode 100644 index 0000000..8bb257c --- /dev/null +++ b/src/routes/api/mappools/[id]/slots/+server.ts @@ -0,0 +1,38 @@ +import { json, error } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { mappoolSlot } from '$lib/server/db/schema'; +import { eq, and } from 'drizzle-orm'; +import { getBeatmap } from '$lib/server/osu/api'; +import type { RequestHandler } from './$types'; + +// Add a slot to a mappool +export const POST: RequestHandler = async ({ params, request, locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + const { beatmapId, category, mods } = await request.json(); + + if (!beatmapId || !category) { + error(400, 'beatmapId and category are required'); + } + + // Count existing slots in this category to auto-increment order + const existing = await db.query.mappoolSlot.findMany({ + where: and( + eq(mappoolSlot.mappoolId, params.id), + eq(mappoolSlot.category, category.toUpperCase()) + ) + }); + + const [created] = await db + .insert(mappoolSlot) + .values({ + mappoolId: params.id, + beatmapId: String(beatmapId), + category: category.toUpperCase(), + orderInCategory: existing.length + 1, + mods: mods ?? [] + }) + .returning(); + + return json(created, { status: 201 }); +}; diff --git a/src/routes/api/mappools/[id]/slots/[slotId]/+server.ts b/src/routes/api/mappools/[id]/slots/[slotId]/+server.ts new file mode 100644 index 0000000..f53f26f --- /dev/null +++ b/src/routes/api/mappools/[id]/slots/[slotId]/+server.ts @@ -0,0 +1,12 @@ +import { json, error } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { mappoolSlot } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import type { RequestHandler } from './$types'; + +export const DELETE: RequestHandler = async ({ params, locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + await db.delete(mappoolSlot).where(eq(mappoolSlot.id, params.slotId)); + return json({ ok: true }); +}; diff --git a/src/routes/api/matches/+server.ts b/src/routes/api/matches/+server.ts new file mode 100644 index 0000000..2069e3f --- /dev/null +++ b/src/routes/api/matches/+server.ts @@ -0,0 +1,42 @@ +// src/routes/api/matches/+server.ts +import { json, error } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { createMatch } from '$lib/server/match/engine'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + const matches = await db.query.match.findMany({ + with: { + participants: { with: { team: true } } + }, + orderBy: (m, { desc }) => [desc(m.createdAt)] + }); + + return json(matches); +}; + +export const POST: RequestHandler = async ({ request, locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + const body = await request.json(); + const { name, config, mappoolId, teams } = body; + + if (!name || !config || !mappoolId || !teams?.length) { + error(400, 'Missing required fields'); + } + + try { + const result = await createMatch({ + name, + config, + mappoolId, + teams, + createdBy: locals.user.id + }); + return json(result, { status: 201 }); + } catch (e: any) { + error(400, e.message); + } +}; diff --git a/src/routes/api/matches/[id]/+server.ts b/src/routes/api/matches/[id]/+server.ts new file mode 100644 index 0000000..3d9709f --- /dev/null +++ b/src/routes/api/matches/[id]/+server.ts @@ -0,0 +1,26 @@ +// src/routes/api/matches/[id]/+server.ts +import { json, error } from '@sveltejs/kit'; +import { getMatchFull, cancelMatch } from '$lib/server/match/engine'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ params, locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + try { + const m = await getMatchFull(params.id); + return json(m); + } catch (e: any) { + error(404, e.message); + } +}; + +export const DELETE: RequestHandler = async ({ params, locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + try { + const m = await cancelMatch(params.id); + return json(m); + } catch (e: any) { + error(400, e.message); + } +}; diff --git a/src/routes/api/matches/[id]/lobby/+server.ts b/src/routes/api/matches/[id]/lobby/+server.ts new file mode 100644 index 0000000..235fb62 --- /dev/null +++ b/src/routes/api/matches/[id]/lobby/+server.ts @@ -0,0 +1,17 @@ +// src/routes/api/matches/[id]/lobby/+server.ts +import { json, error } from '@sveltejs/kit'; +import { moveToLobby } from '$lib/server/match/engine'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ params, request, locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + const body = await request.json().catch(() => ({})); + + try { + const m = await moveToLobby(params.id, body.osuLobbyId); + return json(m); + } catch (e: any) { + error(400, e.message); + } +}; diff --git a/src/routes/api/matches/[id]/pick/+server.ts b/src/routes/api/matches/[id]/pick/+server.ts new file mode 100644 index 0000000..dc13ac7 --- /dev/null +++ b/src/routes/api/matches/[id]/pick/+server.ts @@ -0,0 +1,20 @@ +// src/routes/api/matches/[id]/pick/+server.ts +import { json, error } from '@sveltejs/kit'; +import { pickMap } from '$lib/server/match/engine'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ params, request, locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + const { participantId, mappoolSlotId } = await request.json(); + if (!participantId || !mappoolSlotId) { + error(400, 'participantId and mappoolSlotId required'); + } + + try { + const game = await pickMap(params.id, participantId, mappoolSlotId); + return json(game); + } catch (e: any) { + error(400, e.message); + } +}; diff --git a/src/routes/api/matches/[id]/roll/+server.ts b/src/routes/api/matches/[id]/roll/+server.ts new file mode 100644 index 0000000..d955bba --- /dev/null +++ b/src/routes/api/matches/[id]/roll/+server.ts @@ -0,0 +1,20 @@ +// src/routes/api/matches/[id]/roll/+server.ts +import { json, error } from '@sveltejs/kit'; +import { submitRoll } from '$lib/server/match/engine'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ params, request, locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + const { participantId, value } = await request.json(); + if (!participantId || value == null) { + error(400, 'participantId and value required'); + } + + try { + const m = await submitRoll(params.id, participantId, value); + return json(m); + } catch (e: any) { + error(400, e.message); + } +}; diff --git a/src/routes/api/matches/[id]/rolling/+server.ts b/src/routes/api/matches/[id]/rolling/+server.ts new file mode 100644 index 0000000..f0cf605 --- /dev/null +++ b/src/routes/api/matches/[id]/rolling/+server.ts @@ -0,0 +1,15 @@ +// src/routes/api/matches/[id]/rolling/+server.ts +import { json, error } from '@sveltejs/kit'; +import { moveToRolling } from '$lib/server/match/engine'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ params, locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + try { + const m = await moveToRolling(params.id); + return json(m); + } catch (e: any) { + error(400, e.message); + } +}; diff --git a/src/routes/api/matches/[id]/score/+server.ts b/src/routes/api/matches/[id]/score/+server.ts new file mode 100644 index 0000000..0525002 --- /dev/null +++ b/src/routes/api/matches/[id]/score/+server.ts @@ -0,0 +1,20 @@ +// src/routes/api/matches/[id]/score/+server.ts +import { json, error } from '@sveltejs/kit'; +import { submitGameScores } from '$lib/server/match/engine'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ request, locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + const { matchGameId, scores } = await request.json(); + if (!matchGameId || !scores?.length) { + error(400, 'matchGameId and scores required'); + } + + try { + const m = await submitGameScores(matchGameId, scores); + return json(m); + } catch (e: any) { + error(400, e.message); + } +}; diff --git a/src/routes/api/osu/beatmap/[id]/+server.ts b/src/routes/api/osu/beatmap/[id]/+server.ts new file mode 100644 index 0000000..71f5215 --- /dev/null +++ b/src/routes/api/osu/beatmap/[id]/+server.ts @@ -0,0 +1,14 @@ +import { json, error } from '@sveltejs/kit'; +import { getBeatmap } from '$lib/server/osu/api'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ params, locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + try { + const beatmap = await getBeatmap(params.id); + return json(beatmap); + } catch { + error(404, 'Beatmap not found'); + } +}; diff --git a/src/routes/api/queue/+server.ts b/src/routes/api/queue/+server.ts new file mode 100644 index 0000000..a4d65c6 --- /dev/null +++ b/src/routes/api/queue/+server.ts @@ -0,0 +1,43 @@ +// src/routes/api/queue/+server.ts +import { json, error } from '@sveltejs/kit'; +import { joinQueue, leaveQueue, getQueueStatus } from '$lib/server/match/engine'; +import { db } from '$lib/server/db'; +import { teamMember } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + const status = await getQueueStatus(locals.user.id); + return json(status); +}; + +export const POST: RequestHandler = async ({ locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + // Find user's personal team + const membership = await db.query.teamMember.findFirst({ + where: eq(teamMember.userId, locals.user.id), + with: { team: true } + }); + + const personalTeam = membership?.team; + if (!personalTeam?.isPersonal) { + error(400, 'No personal team found'); + } + + try { + const result = await joinQueue(locals.user.id, personalTeam.id); + return json({ matched: !!result, match: result }); + } catch (e: any) { + error(400, e.message); + } +}; + +export const DELETE: RequestHandler = async ({ locals }) => { + if (!locals.user) error(401, 'Not logged in'); + + await leaveQueue(locals.user.id); + return json({ ok: true }); +}; diff --git a/src/routes/demo/+page.svelte b/src/routes/demo/+page.svelte new file mode 100644 index 0000000..948d26f --- /dev/null +++ b/src/routes/demo/+page.svelte @@ -0,0 +1,5 @@ + + +better-auth diff --git a/src/routes/demo/better-auth/+page.server.ts b/src/routes/demo/better-auth/+page.server.ts new file mode 100644 index 0000000..7c30835 --- /dev/null +++ b/src/routes/demo/better-auth/+page.server.ts @@ -0,0 +1,20 @@ +import { redirect } from '@sveltejs/kit'; +import type { Actions } from './$types'; +import type { PageServerLoad } from './$types'; +import { auth } from '$lib/server/auth'; + +export const load: PageServerLoad = async (event) => { + if (!event.locals.user) { + return redirect(302, '/demo/better-auth/login'); + } + return { user: event.locals.user }; +}; + +export const actions: Actions = { + signOut: async (event) => { + await auth.api.signOut({ + headers: event.request.headers + }); + return redirect(302, '/demo/better-auth/login'); + } +}; diff --git a/src/routes/demo/better-auth/+page.svelte b/src/routes/demo/better-auth/+page.svelte new file mode 100644 index 0000000..f25310a --- /dev/null +++ b/src/routes/demo/better-auth/+page.svelte @@ -0,0 +1,14 @@ + + +

Hi, {data.user.name}!

+

Your user ID is {data.user.id}.

+
+ +
diff --git a/src/routes/demo/better-auth/login/+page.server.ts b/src/routes/demo/better-auth/login/+page.server.ts new file mode 100644 index 0000000..9c25def --- /dev/null +++ b/src/routes/demo/better-auth/login/+page.server.ts @@ -0,0 +1,78 @@ +import { fail, redirect } from '@sveltejs/kit'; +import type { Actions } from './$types'; +import type { PageServerLoad } from './$types'; +import { auth } from '$lib/server/auth'; +import { APIError } from 'better-auth/api'; + +export const load: PageServerLoad = async (event) => { + if (event.locals.user) { + return redirect(302, '/demo/better-auth'); + } + return {}; +}; + +export const actions: Actions = { + signInEmail: async (event) => { + const formData = await event.request.formData(); + const email = formData.get('email')?.toString() ?? ''; + const password = formData.get('password')?.toString() ?? ''; + + try { + await auth.api.signInEmail({ + body: { + email, + password, + callbackURL: '/auth/verification-success' + } + }); + } catch (error) { + if (error instanceof APIError) { + return fail(400, { message: error.message || 'Signin failed' }); + } + return fail(500, { message: 'Unexpected error' }); + } + + return redirect(302, '/demo/better-auth'); + }, + signUpEmail: async (event) => { + const formData = await event.request.formData(); + const email = formData.get('email')?.toString() ?? ''; + const password = formData.get('password')?.toString() ?? ''; + const name = formData.get('name')?.toString() ?? ''; + + try { + await auth.api.signUpEmail({ + body: { + email, + password, + name, + callbackURL: '/auth/verification-success' + } + }); + } catch (error) { + if (error instanceof APIError) { + return fail(400, { message: error.message || 'Registration failed' }); + } + return fail(500, { message: 'Unexpected error' }); + } + + return redirect(302, '/demo/better-auth'); + }, + signInSocial: async (event) => { + const formData = await event.request.formData(); + const provider = formData.get('provider')?.toString() ?? 'osu'; + const callbackURL = formData.get('callbackURL')?.toString() ?? '/demo/better-auth'; + + const result = await auth.api.signInSocial({ + body: { + provider: provider as 'osu', + callbackURL + } + }); + + if (result.url) { + return redirect(302, result.url); + } + return fail(400, { message: 'Social sign-in failed' }); + } +}; diff --git a/src/routes/demo/better-auth/login/+page.svelte b/src/routes/demo/better-auth/login/+page.svelte new file mode 100644 index 0000000..e7f8079 --- /dev/null +++ b/src/routes/demo/better-auth/login/+page.svelte @@ -0,0 +1,51 @@ + + +

Login

+
+ + + + + +
+

{form?.message ?? ''}

+ +
+
+ + + +
diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts new file mode 100644 index 0000000..063dc18 --- /dev/null +++ b/src/routes/login/+page.server.ts @@ -0,0 +1,18 @@ +import { redirect } from '@sveltejs/kit'; +import { auth } from '$lib/server/auth'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async (event) => { + if (event.locals.user) redirect(302, '/'); + + const result = await auth.api.signInSocial({ + body: { + provider: 'osu', + callbackURL: '/' + } + }); + + if (result.url) redirect(302, result.url); + + return {}; +}; diff --git a/static/android-chrome-192x192.png b/static/android-chrome-192x192.png new file mode 100644 index 0000000..e15a9ab Binary files /dev/null and b/static/android-chrome-192x192.png differ diff --git a/static/android-chrome-512x512.png b/static/android-chrome-512x512.png new file mode 100644 index 0000000..58bc4cb Binary files /dev/null and b/static/android-chrome-512x512.png differ diff --git a/static/apple-touch-icon.png b/static/apple-touch-icon.png new file mode 100644 index 0000000..fb8b64b Binary files /dev/null and b/static/apple-touch-icon.png differ diff --git a/static/favicon-16x16.png b/static/favicon-16x16.png new file mode 100644 index 0000000..6700c23 Binary files /dev/null and b/static/favicon-16x16.png differ diff --git a/static/favicon-32x32.png b/static/favicon-32x32.png new file mode 100644 index 0000000..7837088 Binary files /dev/null and b/static/favicon-32x32.png differ diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..2c9e68f Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/static/site.webmanifest b/static/site.webmanifest new file mode 100644 index 0000000..45dc8a2 --- /dev/null +++ b/static/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..e744595 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,6 @@ +import adapter from '@sveltejs/adapter-node'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { kit: { adapter: adapter() } }; + +export default config; diff --git a/tsconfig.json b/tsconfig.json index bfa0fea..2c2ed3c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,29 +1,20 @@ { - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript } diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..125d07c --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,37 @@ +import devtoolsJson from 'vite-plugin-devtools-json'; +import tailwindcss from '@tailwindcss/vite'; +import { defineConfig } from 'vitest/config'; +import { playwright } from '@vitest/browser-playwright'; +import { sveltekit } from '@sveltejs/kit/vite'; + +export default defineConfig({ + plugins: [tailwindcss(), sveltekit(), devtoolsJson()], + test: { + expect: { requireAssertions: true }, + projects: [ + { + extends: './vite.config.ts', + test: { + name: 'client', + browser: { + enabled: true, + provider: playwright(), + instances: [{ browser: 'chromium', headless: true }] + }, + include: ['src/**/*.svelte.{test,spec}.{js,ts}'], + exclude: ['src/lib/server/**'] + } + }, + + { + extends: './vite.config.ts', + test: { + name: 'server', + environment: 'node', + include: ['src/**/*.{test,spec}.{js,ts}'], + exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'] + } + } + ] + } +});