From 034f9df139a8ef024d43349ec7f589c82878db73 Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Sat, 31 Jan 2026 03:06:22 +0100 Subject: [PATCH 01/14] bunch of shit from claude code --- .DS_Store | Bin 0 -> 6148 bytes .env.example | 1 + .gitignore | 3 +- backend/.env.example | 19 + backend/.gitignore | 34 + backend/CLAUDE.md | 106 +++ backend/README.md | 15 + backend/bun.lock | 75 ++ backend/package.json | 23 + backend/src/application/events/EventBus.ts | 56 ++ backend/src/application/events/index.ts | 1 + .../src/application/services/MatchService.ts | 334 +++++++ .../src/application/services/QueueService.ts | 183 ++++ backend/src/application/services/index.ts | 2 + backend/src/cli/index.ts | 262 ++++++ backend/src/domain/events/DomainEvent.ts | 13 + backend/src/domain/events/MatchEvents.ts | 115 +++ backend/src/domain/events/QueueEvents.ts | 35 + backend/src/domain/events/index.ts | 3 + backend/src/domain/interfaces/IGameService.ts | 18 + .../src/domain/interfaces/IMatchProtocol.ts | 0 .../src/domain/interfaces/IMatchRepository.ts | 12 + .../domain/interfaces/INotificationService.ts | 11 + .../domain/interfaces/IPlayerRepository.ts | 10 + .../src/domain/interfaces/IQueueRepository.ts | 15 + .../src/domain/interfaces/ITeamRepository.ts | 9 + backend/src/domain/interfaces/index.ts | 7 + backend/src/domain/models/Match.ts | 71 ++ backend/src/domain/models/MatchState.ts | 20 + backend/src/domain/models/OsuMap.ts | 45 + backend/src/domain/models/Player.ts | 29 + backend/src/domain/models/QueueEntry.ts | 22 + backend/src/domain/models/Score.ts | 38 + backend/src/domain/models/Team.ts | 35 + backend/src/domain/models/index.ts | 7 + .../domain/protocols/HeadToHeadProtocol.ts | 160 ++++ .../src/domain/protocols/IMatchProtocol.ts | 63 ++ .../src/domain/protocols/ProtocolRegistry.ts | 32 + .../domain/protocols/StandardOsuProtocol.ts | 192 ++++ backend/src/domain/protocols/index.ts | 4 + backend/src/index.ts | 197 ++++ .../database/PostgresMatchRepository.ts | 293 ++++++ .../database/PostgresPlayerRepository.ts | 77 ++ .../database/PostgresQueueRepository.ts | 173 ++++ .../database/PostgresTeamRepository.ts | 128 +++ backend/src/infrastructure/database/index.ts | 4 + .../database/migrations/001_initial.sql | 98 ++ .../infrastructure/discord/DiscordService.ts | 174 ++++ backend/src/infrastructure/discord/index.ts | 1 + .../mocks/InMemoryMatchRepository.ts | 63 ++ .../mocks/InMemoryPlayerRepository.ts | 47 + .../mocks/InMemoryQueueRepository.ts | 61 ++ .../mocks/InMemoryTeamRepository.ts | 36 + .../infrastructure/mocks/MockGameService.ts | 136 +++ .../mocks/MockNotificationService.ts | 50 + backend/src/infrastructure/mocks/index.ts | 6 + backend/src/infrastructure/osu/OsuService.ts | 187 ++++ backend/src/infrastructure/osu/index.ts | 1 + .../websocket/WebSocketService.ts | 220 +++++ backend/src/infrastructure/websocket/index.ts | 1 + backend/src/presentation/api/routes.ts | 0 backend/src/testing/TestHarness.ts | 217 +++++ backend/src/testing/index.ts | 1 + .../tests/application/QueueService.test.ts | 97 ++ .../protocols/StandardOsuProtocol.test.ts | 155 ++++ .../tests/integration/full-match-flow.test.ts | 121 +++ tsconfig.json => backend/tsconfig.json | 0 bun.lock | 26 - frontend/.env.example | 2 + frontend/.gitignore | 24 + frontend/.npmrc | 1 + frontend/.prettierignore | 10 + frontend/.prettierrc | 16 + frontend/.vscode/settings.json | 5 + frontend/README.md | 42 + frontend/bun.lock | 854 ++++++++++++++++++ frontend/drizzle.config.ts | 11 + frontend/e2e/demo.test.ts | 6 + frontend/eslint.config.js | 41 + frontend/package.json | 61 ++ frontend/playwright.config.ts | 6 + frontend/src/app.d.ts | 18 + frontend/src/app.html | 11 + frontend/src/demo.spec.ts | 7 + frontend/src/hooks.server.ts | 28 + frontend/src/lib/assets/favicon.svg | 1 + frontend/src/lib/index.ts | 1 + frontend/src/lib/server/auth.ts | 81 ++ frontend/src/lib/server/db/index.ts | 10 + frontend/src/lib/server/db/schema.ts | 20 + frontend/src/routes/+layout.svelte | 9 + frontend/src/routes/+page.svelte | 2 + frontend/src/routes/demo/+page.svelte | 5 + .../src/routes/demo/lucia/+page.server.ts | 31 + frontend/src/routes/demo/lucia/+page.svelte | 12 + .../routes/demo/lucia/login/+page.server.ts | 107 +++ .../src/routes/demo/lucia/login/+page.svelte | 34 + frontend/src/routes/layout.css | 3 + frontend/src/routes/page.svelte.spec.ts | 13 + frontend/static/robots.txt | 3 + frontend/svelte.config.js | 6 + frontend/tsconfig.json | 20 + frontend/vite.config.ts | 43 + index.ts | 1 - package.json | 12 - 105 files changed, 6167 insertions(+), 40 deletions(-) create mode 100644 .DS_Store create mode 100644 .env.example create mode 100644 backend/.env.example create mode 100644 backend/.gitignore create mode 100644 backend/CLAUDE.md create mode 100644 backend/README.md create mode 100644 backend/bun.lock create mode 100644 backend/package.json create mode 100644 backend/src/application/events/EventBus.ts create mode 100644 backend/src/application/events/index.ts create mode 100644 backend/src/application/services/MatchService.ts create mode 100644 backend/src/application/services/QueueService.ts create mode 100644 backend/src/application/services/index.ts create mode 100644 backend/src/cli/index.ts create mode 100644 backend/src/domain/events/DomainEvent.ts create mode 100644 backend/src/domain/events/MatchEvents.ts create mode 100644 backend/src/domain/events/QueueEvents.ts create mode 100644 backend/src/domain/events/index.ts create mode 100644 backend/src/domain/interfaces/IGameService.ts create mode 100644 backend/src/domain/interfaces/IMatchProtocol.ts create mode 100644 backend/src/domain/interfaces/IMatchRepository.ts create mode 100644 backend/src/domain/interfaces/INotificationService.ts create mode 100644 backend/src/domain/interfaces/IPlayerRepository.ts create mode 100644 backend/src/domain/interfaces/IQueueRepository.ts create mode 100644 backend/src/domain/interfaces/ITeamRepository.ts create mode 100644 backend/src/domain/interfaces/index.ts create mode 100644 backend/src/domain/models/Match.ts create mode 100644 backend/src/domain/models/MatchState.ts create mode 100644 backend/src/domain/models/OsuMap.ts create mode 100644 backend/src/domain/models/Player.ts create mode 100644 backend/src/domain/models/QueueEntry.ts create mode 100644 backend/src/domain/models/Score.ts create mode 100644 backend/src/domain/models/Team.ts create mode 100644 backend/src/domain/models/index.ts create mode 100644 backend/src/domain/protocols/HeadToHeadProtocol.ts create mode 100644 backend/src/domain/protocols/IMatchProtocol.ts create mode 100644 backend/src/domain/protocols/ProtocolRegistry.ts create mode 100644 backend/src/domain/protocols/StandardOsuProtocol.ts create mode 100644 backend/src/domain/protocols/index.ts create mode 100644 backend/src/index.ts create mode 100644 backend/src/infrastructure/database/PostgresMatchRepository.ts create mode 100644 backend/src/infrastructure/database/PostgresPlayerRepository.ts create mode 100644 backend/src/infrastructure/database/PostgresQueueRepository.ts create mode 100644 backend/src/infrastructure/database/PostgresTeamRepository.ts create mode 100644 backend/src/infrastructure/database/index.ts create mode 100644 backend/src/infrastructure/database/migrations/001_initial.sql create mode 100644 backend/src/infrastructure/discord/DiscordService.ts create mode 100644 backend/src/infrastructure/discord/index.ts create mode 100644 backend/src/infrastructure/mocks/InMemoryMatchRepository.ts create mode 100644 backend/src/infrastructure/mocks/InMemoryPlayerRepository.ts create mode 100644 backend/src/infrastructure/mocks/InMemoryQueueRepository.ts create mode 100644 backend/src/infrastructure/mocks/InMemoryTeamRepository.ts create mode 100644 backend/src/infrastructure/mocks/MockGameService.ts create mode 100644 backend/src/infrastructure/mocks/MockNotificationService.ts create mode 100644 backend/src/infrastructure/mocks/index.ts create mode 100644 backend/src/infrastructure/osu/OsuService.ts create mode 100644 backend/src/infrastructure/osu/index.ts create mode 100644 backend/src/infrastructure/websocket/WebSocketService.ts create mode 100644 backend/src/infrastructure/websocket/index.ts create mode 100644 backend/src/presentation/api/routes.ts create mode 100644 backend/src/testing/TestHarness.ts create mode 100644 backend/src/testing/index.ts create mode 100644 backend/tests/application/QueueService.test.ts create mode 100644 backend/tests/domain/protocols/StandardOsuProtocol.test.ts create mode 100644 backend/tests/integration/full-match-flow.test.ts rename tsconfig.json => backend/tsconfig.json (100%) delete mode 100644 bun.lock create mode 100644 frontend/.env.example create mode 100644 frontend/.gitignore create mode 100644 frontend/.npmrc create mode 100644 frontend/.prettierignore create mode 100644 frontend/.prettierrc create mode 100644 frontend/.vscode/settings.json create mode 100644 frontend/README.md create mode 100644 frontend/bun.lock create mode 100644 frontend/drizzle.config.ts create mode 100644 frontend/e2e/demo.test.ts create mode 100644 frontend/eslint.config.js create mode 100644 frontend/package.json create mode 100644 frontend/playwright.config.ts create mode 100644 frontend/src/app.d.ts create mode 100644 frontend/src/app.html create mode 100644 frontend/src/demo.spec.ts create mode 100644 frontend/src/hooks.server.ts create mode 100644 frontend/src/lib/assets/favicon.svg create mode 100644 frontend/src/lib/index.ts create mode 100644 frontend/src/lib/server/auth.ts create mode 100644 frontend/src/lib/server/db/index.ts create mode 100644 frontend/src/lib/server/db/schema.ts create mode 100644 frontend/src/routes/+layout.svelte create mode 100644 frontend/src/routes/+page.svelte create mode 100644 frontend/src/routes/demo/+page.svelte create mode 100644 frontend/src/routes/demo/lucia/+page.server.ts create mode 100644 frontend/src/routes/demo/lucia/+page.svelte create mode 100644 frontend/src/routes/demo/lucia/login/+page.server.ts create mode 100644 frontend/src/routes/demo/lucia/login/+page.svelte create mode 100644 frontend/src/routes/layout.css create mode 100644 frontend/src/routes/page.svelte.spec.ts create mode 100644 frontend/static/robots.txt create mode 100644 frontend/svelte.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts delete mode 100644 index.ts delete mode 100644 package.json diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..398ca13b50ddd1a99e76c137217386923b3d9d7d GIT binary patch literal 6148 zcmeHKy-EW?5dJm^UAe??=7_t&%ED8 z>3~8}#|2XKaEfCjb~V9(c4I$}VC!+Z%-7GWdi%1sabxpZcy^M`^Zbs#pLcDW(?%I% zjL`M7M78=hxA-~TXAf)p*ET1K^D`(~{rj-`%=f7%1ImCh@Y4)X&1MN!9O|qLC` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; + +// import .css files directly and it works +import './index.css'; + +import { createRoot } from "react-dom/client"; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..3dd5e6d --- /dev/null +++ b/backend/README.md @@ -0,0 +1,15 @@ +# backend + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.3. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/backend/bun.lock b/backend/bun.lock new file mode 100644 index 0000000..dab0933 --- /dev/null +++ b/backend/bun.lock @@ -0,0 +1,75 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "backend", + "dependencies": { + "discord.js": "^14.14.1", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@discordjs/builders": ["@discordjs/builders@1.13.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.33", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w=="], + + "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], + + "@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="], + + "@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="], + + "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="], + + "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], + + "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], + + "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], + + "@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="], + + "@types/bun": ["@types/bun@1.3.7", "", { "dependencies": { "bun-types": "1.3.7" } }, "sha512-lmNuMda+Z9b7tmhA0tohwy8ZWFSnmQm1UDWXtH5r9F7wZCfkeO3Jx7wKQ1EOiKq43yHts7ky6r8SDJQWRNupkA=="], + + "@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], + + "bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="], + + "discord-api-types": ["discord-api-types@0.38.38", "", {}, "sha512-7qcM5IeZrfb+LXW07HvoI5L+j4PQeMZXEkSm1htHAHh4Y9JSMXBWjy/r7zmUCOj4F7zNjMcm7IMWr131MT2h0Q=="], + + "discord.js": ["discord.js@14.25.1", "", { "dependencies": { "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.33", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + + "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], + + "magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="], + + "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "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=="], + + "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + + "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..5ad313b --- /dev/null +++ b/backend/package.json @@ -0,0 +1,23 @@ +{ + "name": "vash-esports-backend", + "version": "1.0.0", + "module": "src/index.ts", + "type": "module", + "private": true, + "scripts": { + "dev": "bun --hot src/index.ts", + "start": "bun src/index.ts", + "test": "bun test", + "cli": "bun src/cli/index.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "discord.js": "^14.14.1" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/backend/src/application/events/EventBus.ts b/backend/src/application/events/EventBus.ts new file mode 100644 index 0000000..6691ac0 --- /dev/null +++ b/backend/src/application/events/EventBus.ts @@ -0,0 +1,56 @@ +import type { DomainEvent } from "../../domain/events/DomainEvent"; + +type EventHandler = (event: T) => void | Promise; + +export class EventBus { + private handlers: Map> = new Map(); + private allHandlers: Set = new Set(); + + subscribe( + type: T["type"], + handler: EventHandler + ): () => void { + if (!this.handlers.has(type)) { + this.handlers.set(type, new Set()); + } + this.handlers.get(type)!.add(handler as EventHandler); + + return () => { + this.handlers.get(type)?.delete(handler as EventHandler); + }; + } + + subscribeAll(handler: EventHandler): () => void { + this.allHandlers.add(handler); + return () => { + this.allHandlers.delete(handler); + }; + } + + async publish(event: DomainEvent): Promise { + const handlers = this.handlers.get(event.type) ?? new Set(); + + const allPromises: Promise[] = []; + + for (const handler of handlers) { + const result = handler(event); + if (result instanceof Promise) { + allPromises.push(result); + } + } + + for (const handler of this.allHandlers) { + const result = handler(event); + if (result instanceof Promise) { + allPromises.push(result); + } + } + + await Promise.all(allPromises); + } + + clear(): void { + this.handlers.clear(); + this.allHandlers.clear(); + } +} diff --git a/backend/src/application/events/index.ts b/backend/src/application/events/index.ts new file mode 100644 index 0000000..ac9eb5d --- /dev/null +++ b/backend/src/application/events/index.ts @@ -0,0 +1 @@ +export * from "./EventBus"; diff --git a/backend/src/application/services/MatchService.ts b/backend/src/application/services/MatchService.ts new file mode 100644 index 0000000..1743bca --- /dev/null +++ b/backend/src/application/services/MatchService.ts @@ -0,0 +1,334 @@ +import type { IMatchRepository } from "../../domain/interfaces/IMatchRepository"; +import type { IPlayerRepository } from "../../domain/interfaces/IPlayerRepository"; +import type { IGameService } from "../../domain/interfaces/IGameService"; +import type { INotificationService } from "../../domain/interfaces/INotificationService"; +import type { ProtocolRegistry } from "../../domain/protocols/ProtocolRegistry"; +import type { ProtocolContext, RollResult } from "../../domain/protocols/IMatchProtocol"; +import type { EventBus } from "../events/EventBus"; +import type { Team } from "../../domain/models/Team"; +import type { Player } from "../../domain/models/Player"; +import type { OsuMap } from "../../domain/models/OsuMap"; +import type { Score } from "../../domain/models/Score"; +import { type Match, createMatch, allPlayersJoined, getTeamWins } from "../../domain/models/Match"; +import { MatchState } from "../../domain/models/MatchState"; +import { createMatchScore } from "../../domain/models/Score"; +import { MatchEvents } from "../../domain/events/MatchEvents"; + +export class MatchService { + constructor( + private matchRepo: IMatchRepository, + private playerRepo: IPlayerRepository, + private protocolRegistry: ProtocolRegistry, + private gameService: IGameService, + private notificationService: INotificationService, + private eventBus: EventBus + ) { + this.setupGameServiceCallbacks(); + } + + private setupGameServiceCallbacks(): void { + this.gameService.onPlayerJoin(async (osuId, lobbyId) => { + const match = await this.matchRepo.findByLobbyId(lobbyId); + if (!match) return; + + const player = await this.playerRepo.findByOsuId(osuId); + if (!player) return; + + await this.handlePlayerJoined(match.id, player); + }); + + this.gameService.onRoll(async (osuId, lobbyId, roll) => { + const match = await this.matchRepo.findByLobbyId(lobbyId); + if (!match) return; + + const player = await this.playerRepo.findByOsuId(osuId); + if (!player) return; + + await this.handleRoll(match.id, player, roll); + }); + + this.gameService.onMatchFinish(async (lobbyId, scores) => { + const match = await this.matchRepo.findByLobbyId(lobbyId); + if (!match) return; + + await this.handleMapComplete(match.id, scores); + }); + } + + async createMatch(teams: Team[], protocolName: string): Promise { + const protocol = this.protocolRegistry.get(protocolName); + + const match = createMatch({ + teams, + protocol: protocolName, + }); + + await this.matchRepo.save(match); + await this.eventBus.publish(MatchEvents.created(match)); + + return match; + } + + async startMatch(matchId: string): Promise { + const match = await this.matchRepo.findById(matchId); + if (!match) { + throw new Error(`Match ${matchId} not found`); + } + + const protocol = this.protocolRegistry.get(match.protocol); + const context = this.createProtocolContext(match); + + const lobbyId = await this.gameService.createLobby(match); + match.lobbyId = lobbyId; + match.startedAt = new Date(); + + const newState = await protocol.onMatchStart(match, context); + if (newState) { + await this.transitionState(match, newState); + } + + for (const team of match.teams) { + for (const player of team.players) { + await this.gameService.invitePlayer(player, lobbyId); + } + } + + await this.notificationService.announceMatch(match); + await this.matchRepo.save(match); + } + + async handlePlayerJoined(matchId: string, player: Player): Promise { + const match = await this.matchRepo.findById(matchId); + if (!match) return; + + if (match.state !== MatchState.WAITING_PLAYERS) return; + + const isMatchPlayer = match.teams.some((t) => + t.players.some((p) => p.id === player.id) + ); + if (!isMatchPlayer) return; + + match.joinedPlayers.add(player.id); + await this.eventBus.publish(MatchEvents.playerJoined(match, player)); + + if (allPlayersJoined(match)) { + const protocol = this.protocolRegistry.get(match.protocol); + const context = this.createProtocolContext(match); + + const newState = await protocol.onAllPlayersJoined(match, context); + if (newState) { + await this.transitionState(match, newState); + } + } + + await this.matchRepo.save(match); + } + + async handleRoll(matchId: string, player: Player, roll: number): Promise { + const match = await this.matchRepo.findById(matchId); + if (!match) return; + + if (match.state !== MatchState.ROLLING) return; + + const team = match.teams.find((t) => t.players.some((p) => p.id === player.id)); + if (!team) return; + + if (match.rolls.has(team.id)) return; + + match.rolls.set(team.id, roll); + await this.eventBus.publish(MatchEvents.rollCompleted(match, player, roll)); + + const rolls: RollResult[] = Array.from(match.rolls.entries()).map( + ([teamId, rollValue]) => ({ + teamId, + playerId: player.id, + roll: rollValue, + }) + ); + + if (rolls.length === match.teams.length) { + const protocol = this.protocolRegistry.get(match.protocol); + const context = this.createProtocolContext(match); + + const newState = await protocol.onRollComplete(match, rolls, context); + if (newState) { + await this.transitionState(match, newState); + } + } + + await this.matchRepo.save(match); + } + + async handlePick(matchId: string, team: Team, map: OsuMap): Promise { + const match = await this.matchRepo.findById(matchId); + if (!match) return; + + if (match.state !== MatchState.PICKING) return; + + const protocol = this.protocolRegistry.get(match.protocol); + if (!protocol.isValidPick(match, team, map)) return; + + const context = this.createProtocolContext(match); + await this.eventBus.publish(MatchEvents.mapPicked(match, team, map)); + + const newState = await protocol.onPick(match, team, map, context); + if (newState) { + await this.transitionState(match, newState); + } + + await this.matchRepo.save(match); + } + + async handleBan(matchId: string, team: Team, map: OsuMap): Promise { + const match = await this.matchRepo.findById(matchId); + if (!match) return; + + if (match.state !== MatchState.BANNING) return; + + const protocol = this.protocolRegistry.get(match.protocol); + if (!protocol.isValidBan(match, team, map)) return; + + const context = this.createProtocolContext(match); + await this.eventBus.publish(MatchEvents.mapBanned(match, team, map)); + + const newState = await protocol.onBan(match, team, map, context); + if (newState) { + await this.transitionState(match, newState); + } + + await this.matchRepo.save(match); + } + + async handleMapComplete(matchId: string, scores: Score[]): Promise { + const match = await this.matchRepo.findById(matchId); + if (!match) return; + + if (match.state !== MatchState.PLAYING) return; + + const protocol = this.protocolRegistry.get(match.protocol); + const winnerId = protocol.calculateMapWinner(match, scores); + + const teamScores = new Map(); + const playerScores = new Map(); + + for (const team of match.teams) { + const teamPlayerIds = team.players.map((p) => p.id); + const teamTotal = scores + .filter((s) => teamPlayerIds.includes(s.playerId)) + .reduce((sum, s) => sum + s.score, 0); + teamScores.set(team.id, teamTotal); + } + + for (const score of scores) { + playerScores.set(score.playerId, score); + } + + const currentMapId = match.picks[match.picks.length - 1]; + const matchScore = createMatchScore({ + mapId: currentMapId, + teamScores, + playerScores, + winner: winnerId, + }); + + match.scores.push(matchScore); + await this.eventBus.publish(MatchEvents.mapCompleted(match, matchScore)); + await this.notificationService.announceMapResult(match, matchScore); + + const context = this.createProtocolContext(match); + const newState = await protocol.onMapComplete(match, scores, context); + + if (newState === MatchState.COMPLETED) { + const winner = match.teams.find((t) => t.id === protocol.calculateMatchWinner(match)); + if (winner) { + match.winner = winner; + match.endedAt = new Date(); + await this.eventBus.publish(MatchEvents.completed(match, winner)); + await this.notificationService.announceMatchResult(match, winner); + await this.updatePlayerRatings(match); + } + } + + if (newState) { + await this.transitionState(match, newState); + } + + await this.matchRepo.save(match); + } + + async cancelMatch(matchId: string, reason: string): Promise { + const match = await this.matchRepo.findById(matchId); + if (!match) return; + + match.endedAt = new Date(); + await this.transitionState(match, MatchState.CANCELLED); + await this.eventBus.publish(MatchEvents.cancelled(match, reason)); + + if (match.lobbyId) { + await this.gameService.closeLobby(match.lobbyId); + } + + await this.matchRepo.save(match); + } + + private async transitionState(match: Match, newState: MatchState): Promise { + const previousState = match.state; + match.state = newState; + await this.matchRepo.updateState(match.id, newState); + await this.eventBus.publish(MatchEvents.stateChanged(match, previousState)); + } + + private createProtocolContext(match: Match): ProtocolContext { + return { + sendMessage: async (message: string) => { + if (match.lobbyId) { + await this.gameService.sendMessage(match.lobbyId, message); + } + }, + setMap: async (mapId: number) => { + if (match.lobbyId) { + await this.gameService.setMap(match.lobbyId, mapId); + } + }, + startGame: async () => { + if (match.lobbyId) { + await this.gameService.startMatch(match.lobbyId); + } + }, + }; + } + + private async updatePlayerRatings(match: Match): Promise { + if (!match.winner) return; + + const K = 32; + const winningTeam = match.winner; + const losingTeam = match.teams.find((t) => t.id !== winningTeam.id); + if (!losingTeam) return; + + const winnerAvgRating = + winningTeam.players.reduce((sum, p) => sum + p.rating, 0) / + winningTeam.players.length; + const loserAvgRating = + losingTeam.players.reduce((sum, p) => sum + p.rating, 0) / + losingTeam.players.length; + + const expectedWinner = + 1 / (1 + Math.pow(10, (loserAvgRating - winnerAvgRating) / 400)); + const expectedLoser = 1 - expectedWinner; + + const winnerDelta = Math.round(K * (1 - expectedWinner)); + const loserDelta = Math.round(K * (0 - expectedLoser)); + + for (const player of winningTeam.players) { + await this.playerRepo.updateRating(player.id, player.rating + winnerDelta); + } + + for (const player of losingTeam.players) { + await this.playerRepo.updateRating( + player.id, + Math.max(100, player.rating + loserDelta) + ); + } + } +} diff --git a/backend/src/application/services/QueueService.ts b/backend/src/application/services/QueueService.ts new file mode 100644 index 0000000..9eb7142 --- /dev/null +++ b/backend/src/application/services/QueueService.ts @@ -0,0 +1,183 @@ +import type { IQueueRepository } from "../../domain/interfaces/IQueueRepository"; +import type { MatchService } from "./MatchService"; +import type { EventBus } from "../events/EventBus"; +import type { Team } from "../../domain/models/Team"; +import type { Match } from "../../domain/models/Match"; +import { type QueueEntry, createQueueEntry } from "../../domain/models/QueueEntry"; +import { getAverageRating } from "../../domain/models/Team"; +import { QueueEvents } from "../../domain/events/QueueEvents"; + +export interface MatchmakingConfig { + maxRatingDifference: number; + ratingExpansionRate: number; + minQueueTime: number; + protocol: string; +} + +const DEFAULT_CONFIG: MatchmakingConfig = { + maxRatingDifference: 200, + ratingExpansionRate: 50, + minQueueTime: 10000, + protocol: "standard-osu", +}; + +export class QueueService { + private config: MatchmakingConfig; + private matchmakingInterval?: ReturnType; + + constructor( + private queueRepo: IQueueRepository, + private matchService: MatchService, + private eventBus: EventBus, + config?: Partial + ) { + this.config = { ...DEFAULT_CONFIG, ...config }; + } + + async joinQueue(team: Team, queueType: string): Promise { + const existing = await this.queueRepo.findByTeamId(team.id); + if (existing) { + throw new Error(`Team ${team.id} is already in queue`); + } + + const entry = createQueueEntry({ + team, + queueType, + }); + + await this.queueRepo.add(entry); + await this.eventBus.publish(QueueEvents.teamJoined(entry)); + + return entry; + } + + async leaveQueue(teamId: string): Promise { + const entry = await this.queueRepo.findByTeamId(teamId); + if (!entry) { + throw new Error(`Team ${teamId} is not in queue`); + } + + await this.queueRepo.remove(teamId); + await this.eventBus.publish(QueueEvents.teamLeft(teamId, entry.queueType)); + } + + async findMatch(queueType: string): Promise { + const entries = await this.queueRepo.findByType(queueType); + if (entries.length < 2) { + return null; + } + + const matchPair = this.findBestMatch(entries); + if (!matchPair) { + return null; + } + + const [entry1, entry2] = matchPair; + + await this.queueRepo.remove(entry1.team.id); + await this.queueRepo.remove(entry2.team.id); + + const teams = [entry1.team, entry2.team]; + await this.eventBus.publish(QueueEvents.matchFound(teams, queueType)); + + const match = await this.matchService.createMatch(teams, this.config.protocol); + await this.matchService.startMatch(match.id); + + return match; + } + + async runMatchmaking(): Promise { + const queueTypes = new Set(); + const allEntries = await this.queueRepo.findAll(); + + for (const entry of allEntries) { + queueTypes.add(entry.queueType); + } + + for (const queueType of queueTypes) { + await this.findMatch(queueType); + } + } + + startAutoMatchmaking(intervalMs: number = 5000): void { + if (this.matchmakingInterval) { + clearInterval(this.matchmakingInterval); + } + this.matchmakingInterval = setInterval(() => { + this.runMatchmaking().catch(console.error); + }, intervalMs); + } + + stopAutoMatchmaking(): void { + if (this.matchmakingInterval) { + clearInterval(this.matchmakingInterval); + this.matchmakingInterval = undefined; + } + } + + private findBestMatch(entries: QueueEntry[]): [QueueEntry, QueueEntry] | null { + const now = Date.now(); + let bestPair: [QueueEntry, QueueEntry] | null = null; + let bestScore = Infinity; + + for (let i = 0; i < entries.length; i++) { + for (let j = i + 1; j < entries.length; j++) { + const entry1 = entries[i]; + const entry2 = entries[j]; + + if (!entry1 || !entry2) continue; + + const rating1 = getAverageRating(entry1.team); + const rating2 = getAverageRating(entry2.team); + const ratingDiff = Math.abs(rating1 - rating2); + + const waitTime1 = now - entry1.joinedAt.getTime(); + const waitTime2 = now - entry2.joinedAt.getTime(); + const minWaitTime = Math.min(waitTime1, waitTime2); + + if (minWaitTime < this.config.minQueueTime) { + continue; + } + + const expansionMultiplier = Math.floor(minWaitTime / this.config.minQueueTime); + const maxAllowedDiff = + this.config.maxRatingDifference + + expansionMultiplier * this.config.ratingExpansionRate; + + if (ratingDiff > maxAllowedDiff) { + continue; + } + + const score = ratingDiff - minWaitTime / 1000; + + if (score < bestScore) { + bestScore = score; + bestPair = [entry1, entry2]; + } + } + } + + return bestPair; + } + + async getQueueStatus(queueType: string): Promise<{ + count: number; + entries: Array<{ + teamName: string; + rating: number; + waitTime: number; + }>; + }> { + const entries = await this.queueRepo.findByType(queueType); + const now = Date.now(); + + return { + count: entries.length, + entries: entries.map((e) => ({ + teamName: e.team.name, + rating: getAverageRating(e.team), + waitTime: now - e.joinedAt.getTime(), + })), + }; + } +} diff --git a/backend/src/application/services/index.ts b/backend/src/application/services/index.ts new file mode 100644 index 0000000..e35ce21 --- /dev/null +++ b/backend/src/application/services/index.ts @@ -0,0 +1,2 @@ +export * from "./MatchService"; +export * from "./QueueService"; diff --git a/backend/src/cli/index.ts b/backend/src/cli/index.ts new file mode 100644 index 0000000..19bfe29 --- /dev/null +++ b/backend/src/cli/index.ts @@ -0,0 +1,262 @@ +import { parseArgs } from "util"; +import { EventBus } from "../application/events/EventBus"; +import { MatchService } from "../application/services/MatchService"; +import { QueueService } from "../application/services/QueueService"; +import { ProtocolRegistry } from "../domain/protocols/ProtocolRegistry"; +import { StandardOsuProtocol } from "../domain/protocols/StandardOsuProtocol"; +import { HeadToHeadProtocol } from "../domain/protocols/HeadToHeadProtocol"; +import { InMemoryPlayerRepository } from "../infrastructure/mocks/InMemoryPlayerRepository"; +import { InMemoryTeamRepository } from "../infrastructure/mocks/InMemoryTeamRepository"; +import { InMemoryMatchRepository } from "../infrastructure/mocks/InMemoryMatchRepository"; +import { InMemoryQueueRepository } from "../infrastructure/mocks/InMemoryQueueRepository"; +import { MockGameService } from "../infrastructure/mocks/MockGameService"; +import { MockNotificationService } from "../infrastructure/mocks/MockNotificationService"; +import { createPlayer } from "../domain/models/Player"; +import { createTeam, getAverageRating } from "../domain/models/Team"; +import { MatchState } from "../domain/models/MatchState"; + +const eventBus = new EventBus(); +const mockGameService = new MockGameService(); +const mockNotificationService = new MockNotificationService(); + +const playerRepo = new InMemoryPlayerRepository(); +const teamRepo = new InMemoryTeamRepository(); +const matchRepo = new InMemoryMatchRepository(); +const queueRepo = new InMemoryQueueRepository(); + +const protocolRegistry = new ProtocolRegistry(); +protocolRegistry.register(new StandardOsuProtocol()); +protocolRegistry.register(new HeadToHeadProtocol()); + +const matchService = new MatchService( + matchRepo, + playerRepo, + protocolRegistry, + mockGameService, + mockNotificationService, + eventBus +); + +const queueService = new QueueService(queueRepo, matchService, eventBus, { + minQueueTime: 0, +}); + +eventBus.subscribeAll((event) => { + console.log(`[Event] ${event.type}`); +}); + +type CommandHandler = (...args: string[]) => Promise; + +const commands: Record = { + "player:create": async (osuName?: string, rating?: string) => { + const player = createPlayer({ + discordId: `discord-${Date.now()}`, + osuId: Math.floor(Math.random() * 1000000), + osuName: osuName ?? `Player${Date.now()}`, + rating: rating ? parseInt(rating) : 1000, + }); + await playerRepo.save(player); + console.log(`Created player: ${player.id} (${player.osuName}, rating: ${player.rating})`); + }, + + "player:list": async () => { + const players = await playerRepo.findAll(); + if (players.length === 0) { + console.log("No players found"); + return; + } + console.log("Players:"); + for (const p of players) { + console.log(` ${p.id}: ${p.osuName} (rating: ${p.rating})`); + } + }, + + "team:create": async (name: string, ...playerIds: string[]) => { + if (!name || playerIds.length === 0) { + console.log("Usage: team:create [playerId2] ..."); + return; + } + const players = await Promise.all( + playerIds.map((id) => playerRepo.findById(id)) + ); + const validPlayers = players.filter((p) => p !== null); + if (validPlayers.length === 0) { + console.log("No valid players found"); + return; + } + const team = createTeam({ name, players: validPlayers }); + await teamRepo.save(team); + console.log(`Created team: ${team.id} (${team.name}, avg rating: ${getAverageRating(team)})`); + }, + + "team:list": async () => { + const teams = await teamRepo.findAll(); + if (teams.length === 0) { + console.log("No teams found"); + return; + } + console.log("Teams:"); + for (const t of teams) { + console.log(` ${t.id}: ${t.name} (${t.players.length} players, avg: ${getAverageRating(t)})`); + } + }, + + "queue:join": async (teamId: string, queueType: string = "ranked") => { + if (!teamId) { + console.log("Usage: queue:join [queueType]"); + return; + } + const team = await teamRepo.findById(teamId); + if (!team) { + console.log(`Team ${teamId} not found`); + return; + } + try { + const entry = await queueService.joinQueue(team, queueType); + console.log(`Team ${team.name} joined ${queueType} queue at ${entry.joinedAt.toISOString()}`); + } catch (e) { + console.log(`Error: ${(e as Error).message}`); + } + }, + + "queue:leave": async (teamId: string) => { + if (!teamId) { + console.log("Usage: queue:leave "); + return; + } + try { + await queueService.leaveQueue(teamId); + console.log(`Team ${teamId} left the queue`); + } catch (e) { + console.log(`Error: ${(e as Error).message}`); + } + }, + + "queue:list": async (queueType: string = "ranked") => { + const status = await queueService.getQueueStatus(queueType); + console.log(`Queue (${queueType}): ${status.count} teams`); + for (const e of status.entries) { + console.log(` ${e.teamName} (rating: ${e.rating}, wait: ${Math.round(e.waitTime / 1000)}s)`); + } + }, + + "matchmaking:run": async () => { + const match = await queueService.findMatch("ranked"); + if (match) { + console.log(`Created match ${match.id}: ${match.teams.map((t) => t.name).join(" vs ")}`); + } else { + console.log("No match found (need 2+ teams in queue)"); + } + }, + + "match:status": async (matchId: string) => { + if (!matchId) { + console.log("Usage: match:status "); + return; + } + const match = await matchRepo.findById(matchId); + if (!match) { + console.log(`Match ${matchId} not found`); + return; + } + console.log(`Match: ${match.id}`); + console.log(` State: ${match.state}`); + console.log(` Protocol: ${match.protocol}`); + console.log(` Teams: ${match.teams.map((t) => t.name).join(" vs ")}`); + console.log(` Scores: ${match.teams.map((t) => `${t.name}: ${match.scores.filter((s) => s.winner === t.id).length}`).join(", ")}`); + if (match.winner) { + console.log(` Winner: ${match.winner.name}`); + } + }, + + "match:list": async () => { + const matches = await matchRepo.findAll(); + if (matches.length === 0) { + console.log("No matches found"); + return; + } + console.log("Matches:"); + for (const m of matches) { + console.log(` ${m.id}: ${m.teams.map((t) => t.name).join(" vs ")} [${m.state}]`); + } + }, + + "match:simulate-join": async (matchId: string, osuIdStr: string) => { + if (!matchId || !osuIdStr) { + console.log("Usage: match:simulate-join "); + return; + } + const match = await matchRepo.findById(matchId); + if (!match || !match.lobbyId) { + console.log(`Match ${matchId} not found or has no lobby`); + return; + } + const osuId = parseInt(osuIdStr); + mockGameService.simulatePlayerJoin(match.lobbyId, osuId); + console.log(`Simulated player ${osuId} joining lobby ${match.lobbyId}`); + }, + + "match:simulate-roll": async (matchId: string, osuIdStr: string, rollStr: string) => { + if (!matchId || !osuIdStr || !rollStr) { + console.log("Usage: match:simulate-roll "); + return; + } + const match = await matchRepo.findById(matchId); + if (!match || !match.lobbyId) { + console.log(`Match ${matchId} not found or has no lobby`); + return; + } + const osuId = parseInt(osuIdStr); + const roll = parseInt(rollStr); + mockGameService.simulateRoll(match.lobbyId, osuId, roll); + console.log(`Simulated roll ${roll} from player ${osuId}`); + }, + + "protocol:list": async () => { + const protocols = protocolRegistry.list(); + console.log("Available protocols:"); + for (const p of protocols) { + console.log(` ${p.name}: ${p.description} (${p.teamSize}v${p.teamSize}, best of ${p.bestOf})`); + } + }, + + help: async () => { + console.log("Available commands:"); + console.log(" player:create [osuName] [rating]"); + console.log(" player:list"); + console.log(" team:create [playerId2] ..."); + console.log(" team:list"); + console.log(" queue:join [queueType]"); + console.log(" queue:leave "); + console.log(" queue:list [queueType]"); + console.log(" matchmaking:run"); + console.log(" match:status "); + console.log(" match:list"); + console.log(" match:simulate-join "); + console.log(" match:simulate-roll "); + console.log(" protocol:list"); + console.log(" help"); + }, +}; + +async function main() { + const args = process.argv.slice(2); + const command = args[0]; + const commandArgs = args.slice(1); + + if (!command) { + await commands.help(); + process.exit(0); + } + + const handler = commands[command]; + if (!handler) { + console.log(`Unknown command: ${command}`); + await commands.help(); + process.exit(1); + } + + await handler(...commandArgs); +} + +main().catch(console.error); diff --git a/backend/src/domain/events/DomainEvent.ts b/backend/src/domain/events/DomainEvent.ts new file mode 100644 index 0000000..7a32728 --- /dev/null +++ b/backend/src/domain/events/DomainEvent.ts @@ -0,0 +1,13 @@ +export interface DomainEvent { + type: string; + timestamp: Date; + payload: T; +} + +export function createEvent(type: string, payload: T): DomainEvent { + return { + type, + timestamp: new Date(), + payload, + }; +} diff --git a/backend/src/domain/events/MatchEvents.ts b/backend/src/domain/events/MatchEvents.ts new file mode 100644 index 0000000..8ebb232 --- /dev/null +++ b/backend/src/domain/events/MatchEvents.ts @@ -0,0 +1,115 @@ +import type { Match } from "../models/Match"; +import type { MatchState } from "../models/MatchState"; +import type { Player } from "../models/Player"; +import type { Team } from "../models/Team"; +import type { MatchScore } from "../models/Score"; +import type { OsuMap } from "../models/OsuMap"; +import type { DomainEvent } from "./DomainEvent"; +import { createEvent } from "./DomainEvent"; + +export interface MatchCreatedPayload { + match: Match; +} + +export interface MatchStateChangedPayload { + match: Match; + previousState: MatchState; +} + +export interface PlayerJoinedLobbyPayload { + match: Match; + player: Player; +} + +export interface RollCompletedPayload { + match: Match; + player: Player; + roll: number; +} + +export interface MapPickedPayload { + match: Match; + team: Team; + map: OsuMap; +} + +export interface MapBannedPayload { + match: Match; + team: Team; + map: OsuMap; +} + +export interface MapStartedPayload { + match: Match; + map: OsuMap; +} + +export interface MapCompletedPayload { + match: Match; + scores: MatchScore; +} + +export interface MatchCompletedPayload { + match: Match; + winner: Team; +} + +export interface MatchCancelledPayload { + match: Match; + reason: string; +} + +export type MatchCreatedEvent = DomainEvent & { type: "match.created" }; +export type MatchStateChangedEvent = DomainEvent & { type: "match.state_changed" }; +export type PlayerJoinedLobbyEvent = DomainEvent & { type: "match.player_joined" }; +export type RollCompletedEvent = DomainEvent & { type: "match.roll_completed" }; +export type MapPickedEvent = DomainEvent & { type: "match.map_picked" }; +export type MapBannedEvent = DomainEvent & { type: "match.map_banned" }; +export type MapStartedEvent = DomainEvent & { type: "match.map_started" }; +export type MapCompletedEvent = DomainEvent & { type: "match.map_completed" }; +export type MatchCompletedEvent = DomainEvent & { type: "match.completed" }; +export type MatchCancelledEvent = DomainEvent & { type: "match.cancelled" }; + +export type MatchEvent = + | MatchCreatedEvent + | MatchStateChangedEvent + | PlayerJoinedLobbyEvent + | RollCompletedEvent + | MapPickedEvent + | MapBannedEvent + | MapStartedEvent + | MapCompletedEvent + | MatchCompletedEvent + | MatchCancelledEvent; + +export const MatchEvents = { + created: (match: Match): MatchCreatedEvent => + createEvent("match.created", { match }) as MatchCreatedEvent, + + stateChanged: (match: Match, previousState: MatchState): MatchStateChangedEvent => + createEvent("match.state_changed", { match, previousState }) as MatchStateChangedEvent, + + playerJoined: (match: Match, player: Player): PlayerJoinedLobbyEvent => + createEvent("match.player_joined", { match, player }) as PlayerJoinedLobbyEvent, + + rollCompleted: (match: Match, player: Player, roll: number): RollCompletedEvent => + createEvent("match.roll_completed", { match, player, roll }) as RollCompletedEvent, + + mapPicked: (match: Match, team: Team, map: OsuMap): MapPickedEvent => + createEvent("match.map_picked", { match, team, map }) as MapPickedEvent, + + mapBanned: (match: Match, team: Team, map: OsuMap): MapBannedEvent => + createEvent("match.map_banned", { match, team, map }) as MapBannedEvent, + + mapStarted: (match: Match, map: OsuMap): MapStartedEvent => + createEvent("match.map_started", { match, map }) as MapStartedEvent, + + mapCompleted: (match: Match, scores: MatchScore): MapCompletedEvent => + createEvent("match.map_completed", { match, scores }) as MapCompletedEvent, + + completed: (match: Match, winner: Team): MatchCompletedEvent => + createEvent("match.completed", { match, winner }) as MatchCompletedEvent, + + cancelled: (match: Match, reason: string): MatchCancelledEvent => + createEvent("match.cancelled", { match, reason }) as MatchCancelledEvent, +}; diff --git a/backend/src/domain/events/QueueEvents.ts b/backend/src/domain/events/QueueEvents.ts new file mode 100644 index 0000000..77e0058 --- /dev/null +++ b/backend/src/domain/events/QueueEvents.ts @@ -0,0 +1,35 @@ +import type { Team } from "../models/Team"; +import type { QueueEntry } from "../models/QueueEntry"; +import type { DomainEvent } from "./DomainEvent"; +import { createEvent } from "./DomainEvent"; + +export interface TeamQueuedPayload { + entry: QueueEntry; +} + +export interface TeamDequeuedPayload { + teamId: string; + queueType: string; +} + +export interface MatchFoundPayload { + teams: Team[]; + queueType: string; +} + +export type TeamQueuedEvent = DomainEvent & { type: "queue.team_joined" }; +export type TeamDequeuedEvent = DomainEvent & { type: "queue.team_left" }; +export type MatchFoundEvent = DomainEvent & { type: "queue.match_found" }; + +export type QueueEvent = TeamQueuedEvent | TeamDequeuedEvent | MatchFoundEvent; + +export const QueueEvents = { + teamJoined: (entry: QueueEntry): TeamQueuedEvent => + createEvent("queue.team_joined", { entry }) as TeamQueuedEvent, + + teamLeft: (teamId: string, queueType: string): TeamDequeuedEvent => + createEvent("queue.team_left", { teamId, queueType }) as TeamDequeuedEvent, + + matchFound: (teams: Team[], queueType: string): MatchFoundEvent => + createEvent("queue.match_found", { teams, queueType }) as MatchFoundEvent, +}; diff --git a/backend/src/domain/events/index.ts b/backend/src/domain/events/index.ts new file mode 100644 index 0000000..9d7d966 --- /dev/null +++ b/backend/src/domain/events/index.ts @@ -0,0 +1,3 @@ +export * from "./DomainEvent"; +export * from "./MatchEvents"; +export * from "./QueueEvents"; diff --git a/backend/src/domain/interfaces/IGameService.ts b/backend/src/domain/interfaces/IGameService.ts new file mode 100644 index 0000000..4063bc9 --- /dev/null +++ b/backend/src/domain/interfaces/IGameService.ts @@ -0,0 +1,18 @@ +import type { Match } from "../models/Match"; +import type { Player } from "../models/Player"; +import type { Score } from "../models/Score"; + +export interface IGameService { + createLobby(match: Match): Promise; + closeLobby(lobbyId: string): Promise; + invitePlayer(player: Player, lobbyId: string): Promise; + sendMessage(lobbyId: string, message: string): Promise; + setMap(lobbyId: string, mapId: number): Promise; + startMatch(lobbyId: string): Promise; + + onPlayerJoin(callback: (osuId: number, lobbyId: string) => void): void; + onPlayerLeave(callback: (osuId: number, lobbyId: string) => void): void; + onMatchStart(callback: (lobbyId: string) => void): void; + onMatchFinish(callback: (lobbyId: string, scores: Score[]) => void): void; + onRoll(callback: (osuId: number, lobbyId: string, roll: number) => void): void; +} diff --git a/backend/src/domain/interfaces/IMatchProtocol.ts b/backend/src/domain/interfaces/IMatchProtocol.ts new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/domain/interfaces/IMatchRepository.ts b/backend/src/domain/interfaces/IMatchRepository.ts new file mode 100644 index 0000000..f386c95 --- /dev/null +++ b/backend/src/domain/interfaces/IMatchRepository.ts @@ -0,0 +1,12 @@ +import type { Match } from "../models/Match"; +import type { MatchState } from "../models/MatchState"; + +export interface IMatchRepository { + findById(id: string): Promise; + findActive(): Promise; + findByPlayer(playerId: string, limit?: number): Promise; + findByLobbyId(lobbyId: string): Promise; + save(match: Match): Promise; + updateState(matchId: string, state: MatchState): Promise; + findAll(): Promise; +} diff --git a/backend/src/domain/interfaces/INotificationService.ts b/backend/src/domain/interfaces/INotificationService.ts new file mode 100644 index 0000000..a2165f8 --- /dev/null +++ b/backend/src/domain/interfaces/INotificationService.ts @@ -0,0 +1,11 @@ +import type { Match } from "../models/Match"; +import type { Team } from "../models/Team"; +import type { MatchScore } from "../models/Score"; + +export interface INotificationService { + announceMatch(match: Match): Promise; + announceMatchResult(match: Match, winner: Team): Promise; + notifyPlayer(playerId: string, message: string): Promise; + notifyTeam(teamId: string, message: string): Promise; + announceMapResult(match: Match, scores: MatchScore): Promise; +} diff --git a/backend/src/domain/interfaces/IPlayerRepository.ts b/backend/src/domain/interfaces/IPlayerRepository.ts new file mode 100644 index 0000000..1df6117 --- /dev/null +++ b/backend/src/domain/interfaces/IPlayerRepository.ts @@ -0,0 +1,10 @@ +import type { Player } from "../models/Player"; + +export interface IPlayerRepository { + findById(id: string): Promise; + findByOsuId(osuId: number): Promise; + findByDiscordId(discordId: string): Promise; + save(player: Player): Promise; + updateRating(playerId: string, newRating: number): Promise; + findAll(): Promise; +} diff --git a/backend/src/domain/interfaces/IQueueRepository.ts b/backend/src/domain/interfaces/IQueueRepository.ts new file mode 100644 index 0000000..8bdb222 --- /dev/null +++ b/backend/src/domain/interfaces/IQueueRepository.ts @@ -0,0 +1,15 @@ +import type { QueueEntry } from "../models/QueueEntry"; + +export interface IQueueRepository { + add(entry: QueueEntry): Promise; + remove(teamId: string): Promise; + findByTeamId(teamId: string): Promise; + findByType(queueType: string): Promise; + findClosestByRating( + rating: number, + queueType: string, + exclude?: string[] + ): Promise; + findAll(): Promise; + clear(): Promise; +} diff --git a/backend/src/domain/interfaces/ITeamRepository.ts b/backend/src/domain/interfaces/ITeamRepository.ts new file mode 100644 index 0000000..516a5d7 --- /dev/null +++ b/backend/src/domain/interfaces/ITeamRepository.ts @@ -0,0 +1,9 @@ +import type { Team } from "../models/Team"; + +export interface ITeamRepository { + findById(id: string): Promise; + findByPlayer(playerId: string): Promise; + save(team: Team): Promise; + delete(teamId: string): Promise; + findAll(): Promise; +} diff --git a/backend/src/domain/interfaces/index.ts b/backend/src/domain/interfaces/index.ts new file mode 100644 index 0000000..4bc264b --- /dev/null +++ b/backend/src/domain/interfaces/index.ts @@ -0,0 +1,7 @@ +export * from "./IPlayerRepository"; +export * from "./ITeamRepository"; +export * from "./IMatchRepository"; +export * from "./IQueueRepository"; +export * from "./IGameService"; +export * from "./INotificationService"; +export * from "./IMatchProtocol"; diff --git a/backend/src/domain/models/Match.ts b/backend/src/domain/models/Match.ts new file mode 100644 index 0000000..dfd3a33 --- /dev/null +++ b/backend/src/domain/models/Match.ts @@ -0,0 +1,71 @@ +import type { Team } from "./Team"; +import type { MatchScore } from "./Score"; +import { MatchState } from "./MatchState"; + +export interface Match { + id: string; + teams: Team[]; + protocol: string; + state: MatchState; + scores: MatchScore[]; + winner?: Team; + lobbyId?: string; + createdAt: Date; + startedAt?: Date; + endedAt?: Date; + + rolls: Map; + bans: Map; + picks: number[]; + currentPicker?: string; + currentBanner?: string; + joinedPlayers: Set; +} + +export function createMatch(params: { + id?: string; + teams: Team[]; + protocol: string; + state?: MatchState; + lobbyId?: string; + createdAt?: Date; +}): Match { + if (params.teams.length < 2) { + throw new Error("Match must have at least two teams"); + } + + return { + id: params.id ?? crypto.randomUUID(), + teams: params.teams, + protocol: params.protocol, + state: params.state ?? MatchState.CREATED, + scores: [], + lobbyId: params.lobbyId, + createdAt: params.createdAt ?? new Date(), + rolls: new Map(), + bans: new Map(), + picks: [], + joinedPlayers: new Set(), + }; +} + +export function getTeamWins(match: Match, teamId: string): number { + return match.scores.filter((s) => s.winner === teamId).length; +} + +export function getMatchWinner(match: Match, bestOf: number): Team | null { + const winsNeeded = Math.ceil(bestOf / 2); + + for (const team of match.teams) { + if (getTeamWins(match, team.id) >= winsNeeded) { + return team; + } + } + + return null; +} + +export function allPlayersJoined(match: Match): boolean { + const allPlayerIds = match.teams.flatMap((t) => t.players.map((p) => p.id)); + return allPlayerIds.every((id) => match.joinedPlayers.has(id)); +} diff --git a/backend/src/domain/models/MatchState.ts b/backend/src/domain/models/MatchState.ts new file mode 100644 index 0000000..7419610 --- /dev/null +++ b/backend/src/domain/models/MatchState.ts @@ -0,0 +1,20 @@ +export enum MatchState { + CREATED = "created", + LOBBY_CREATED = "lobby_created", + WAITING_PLAYERS = "waiting_players", + ROLLING = "rolling", + PICKING = "picking", + BANNING = "banning", + PLAYING = "playing", + MAP_COMPLETE = "map_complete", + COMPLETED = "completed", + CANCELLED = "cancelled", +} + +export function isTerminalState(state: MatchState): boolean { + return state === MatchState.COMPLETED || state === MatchState.CANCELLED; +} + +export function isActiveState(state: MatchState): boolean { + return !isTerminalState(state); +} diff --git a/backend/src/domain/models/OsuMap.ts b/backend/src/domain/models/OsuMap.ts new file mode 100644 index 0000000..b7c6354 --- /dev/null +++ b/backend/src/domain/models/OsuMap.ts @@ -0,0 +1,45 @@ +export interface OsuMap { + id: number; + beatmapSetId: number; + title: string; + artist: string; + difficulty: string; + starRating: number; + bpm: number; + length: number; + mod?: string; +} + +export interface Mappool { + id: string; + name: string; + maps: OsuMap[]; +} + +export function createOsuMap(params: { + id: number; + beatmapSetId?: number; + title: string; + artist: string; + difficulty: string; + starRating?: number; + bpm?: number; + length?: number; + mod?: string; +}): OsuMap { + return { + id: params.id, + beatmapSetId: params.beatmapSetId ?? 0, + title: params.title, + artist: params.artist, + difficulty: params.difficulty, + starRating: params.starRating ?? 0, + bpm: params.bpm ?? 0, + length: params.length ?? 0, + mod: params.mod, + }; +} + +export function getMapsByMod(mappool: Mappool, mod: string): OsuMap[] { + return mappool.maps.filter((m) => m.mod === mod); +} diff --git a/backend/src/domain/models/Player.ts b/backend/src/domain/models/Player.ts new file mode 100644 index 0000000..078224d --- /dev/null +++ b/backend/src/domain/models/Player.ts @@ -0,0 +1,29 @@ +export interface Player { + id: string; + discordId: string; + osuId: number; + osuName: string; + displayName: string; + rating: number; + createdAt: Date; +} + +export function createPlayer(params: { + id?: string; + discordId: string; + osuId: number; + osuName: string; + displayName?: string; + rating?: number; + createdAt?: Date; +}): Player { + return { + id: params.id ?? crypto.randomUUID(), + discordId: params.discordId, + osuId: params.osuId, + osuName: params.osuName, + displayName: params.displayName ?? params.osuName, + rating: params.rating ?? 1000, + createdAt: params.createdAt ?? new Date(), + }; +} diff --git a/backend/src/domain/models/QueueEntry.ts b/backend/src/domain/models/QueueEntry.ts new file mode 100644 index 0000000..0bd159f --- /dev/null +++ b/backend/src/domain/models/QueueEntry.ts @@ -0,0 +1,22 @@ +import type { Team } from "./Team"; + +export interface QueueEntry { + id: string; + team: Team; + queueType: string; + joinedAt: Date; +} + +export function createQueueEntry(params: { + id?: string; + team: Team; + queueType: string; + joinedAt?: Date; +}): QueueEntry { + return { + id: params.id ?? crypto.randomUUID(), + team: params.team, + queueType: params.queueType, + joinedAt: params.joinedAt ?? new Date(), + }; +} diff --git a/backend/src/domain/models/Score.ts b/backend/src/domain/models/Score.ts new file mode 100644 index 0000000..16e4c15 --- /dev/null +++ b/backend/src/domain/models/Score.ts @@ -0,0 +1,38 @@ +export interface Score { + playerId: string; + score: number; + accuracy: number; + maxCombo: number; + misses: number; + mods?: string[]; +} + +export interface MatchScore { + id: string; + mapId: number; + teamScores: Map; + playerScores: Map; + winner: string; +} + +export function createMatchScore(params: { + id?: string; + mapId: number; + teamScores: Map; + playerScores: Map; + winner: string; +}): MatchScore { + return { + id: params.id ?? crypto.randomUUID(), + mapId: params.mapId, + teamScores: params.teamScores, + playerScores: params.playerScores, + winner: params.winner, + }; +} + +export function calculateTeamScore(scores: Score[], teamPlayerIds: string[]): number { + return scores + .filter((s) => teamPlayerIds.includes(s.playerId)) + .reduce((sum, s) => sum + s.score, 0); +} diff --git a/backend/src/domain/models/Team.ts b/backend/src/domain/models/Team.ts new file mode 100644 index 0000000..6e8bc39 --- /dev/null +++ b/backend/src/domain/models/Team.ts @@ -0,0 +1,35 @@ +import type { Player } from "./Player"; + +export interface Team { + id: string; + name: string; + players: Player[]; + captain: Player; + createdAt: Date; +} + +export function createTeam(params: { + id?: string; + name: string; + players: Player[]; + captain?: Player; + createdAt?: Date; +}): Team { + if (params.players.length === 0) { + throw new Error("Team must have at least one player"); + } + + return { + id: params.id ?? crypto.randomUUID(), + name: params.name, + players: params.players, + captain: params.captain ?? params.players[0], + createdAt: params.createdAt ?? new Date(), + }; +} + +export function getAverageRating(team: Team): number { + if (team.players.length === 0) return 0; + const total = team.players.reduce((sum, p) => sum + p.rating, 0); + return Math.round(total / team.players.length); +} diff --git a/backend/src/domain/models/index.ts b/backend/src/domain/models/index.ts new file mode 100644 index 0000000..4263f9e --- /dev/null +++ b/backend/src/domain/models/index.ts @@ -0,0 +1,7 @@ +export * from "./Player"; +export * from "./Team"; +export * from "./Match"; +export * from "./MatchState"; +export * from "./Score"; +export * from "./QueueEntry"; +export * from "./OsuMap"; diff --git a/backend/src/domain/protocols/HeadToHeadProtocol.ts b/backend/src/domain/protocols/HeadToHeadProtocol.ts new file mode 100644 index 0000000..7a67378 --- /dev/null +++ b/backend/src/domain/protocols/HeadToHeadProtocol.ts @@ -0,0 +1,160 @@ +import type { + IMatchProtocol, + ProtocolContext, + RollResult, +} from "./IMatchProtocol"; +import type { Match } from "../models/Match"; +import { MatchState } from "../models/MatchState"; +import type { Team } from "../models/Team"; +import type { OsuMap } from "../models/OsuMap"; +import type { Score } from "../models/Score"; +import { getTeamWins } from "../models/Match"; + +export class HeadToHeadProtocol implements IMatchProtocol { + readonly name = "head-to-head"; + readonly description = "1v1 head-to-head format"; + readonly teamSize = 1; + readonly bestOf = 5; + readonly hasPickBan = false; + readonly hasFreemod = false; + + async onMatchStart( + match: Match, + context: ProtocolContext + ): Promise { + const team1 = match.teams[0]; + const team2 = match.teams[1]; + await context.sendMessage(`1v1 Match: ${team1.name} vs ${team2.name}`); + await context.sendMessage("Waiting for players to join..."); + return MatchState.WAITING_PLAYERS; + } + + async onAllPlayersJoined( + _match: Match, + context: ProtocolContext + ): Promise { + await context.sendMessage("Both players have joined! Please roll (!roll)"); + return MatchState.ROLLING; + } + + async onRollComplete( + match: Match, + rolls: RollResult[], + context: ProtocolContext + ): Promise { + const team1Roll = rolls.find((r) => r.teamId === match.teams[0].id); + const team2Roll = rolls.find((r) => r.teamId === match.teams[1].id); + + if (!team1Roll || !team2Roll) { + return null; + } + + const winningTeam = + team1Roll.roll > team2Roll.roll ? match.teams[0] : match.teams[1]; + + match.currentPicker = winningTeam.id; + + await context.sendMessage( + `${winningTeam.name} wins the roll and picks first!` + ); + + return MatchState.PICKING; + } + + async onBan( + _match: Match, + _team: Team, + _map: OsuMap, + _context: ProtocolContext + ): Promise { + return null; + } + + async onPick( + match: Match, + team: Team, + map: OsuMap, + context: ProtocolContext + ): Promise { + match.picks.push(map.id); + await context.sendMessage(`${team.name} picked ${map.title} [${map.difficulty}]`); + await context.setMap(map.id); + + return MatchState.PLAYING; + } + + async onMapComplete( + match: Match, + _scores: Score[], + context: ProtocolContext + ): Promise { + const winner = this.calculateMatchWinner(match); + if (winner) { + return MatchState.COMPLETED; + } + + const currentPicker = match.currentPicker; + match.currentPicker = + currentPicker === match.teams[0].id + ? match.teams[1].id + : match.teams[0].id; + + const team1 = match.teams[0]; + const team2 = match.teams[1]; + await context.sendMessage( + `Score: ${team1.name} ${getTeamWins(match, team1.id)} - ${getTeamWins(match, team2.id)} ${team2.name}` + ); + + return MatchState.PICKING; + } + + isValidPick(match: Match, team: Team, map: OsuMap): boolean { + if (match.currentPicker !== team.id) return false; + if (match.picks.includes(map.id)) return false; + return true; + } + + isValidBan(_match: Match, _team: Team, _map: OsuMap): boolean { + return false; + } + + calculateMapWinner(match: Match, scores: Score[]): string { + const team1 = match.teams[0]; + const team2 = match.teams[1]; + + const team1Score = scores.find((s) => + team1.players.some((p) => p.id === s.playerId) + )?.score ?? 0; + + const team2Score = scores.find((s) => + team2.players.some((p) => p.id === s.playerId) + )?.score ?? 0; + + return team1Score > team2Score ? team1.id : team2.id; + } + + calculateMatchWinner(match: Match): string | null { + const winsNeeded = this.getWinsNeeded(); + + for (const team of match.teams) { + if (getTeamWins(match, team.id) >= winsNeeded) { + return team.id; + } + } + + return null; + } + + getPickingTeam(match: Match): Team { + const team = match.teams.find((t) => t.id === match.currentPicker); + return team ?? match.teams[0]; + } + + getBanningTeam(match: Match): Team { + return match.teams[0]; + } + + getWinsNeeded(): number { + return Math.ceil(this.bestOf / 2); + } +} diff --git a/backend/src/domain/protocols/IMatchProtocol.ts b/backend/src/domain/protocols/IMatchProtocol.ts new file mode 100644 index 0000000..8a8551e --- /dev/null +++ b/backend/src/domain/protocols/IMatchProtocol.ts @@ -0,0 +1,63 @@ +import type { Match } from "../models/Match"; +import type { MatchState } from "../models/MatchState"; +import type { Team } from "../models/Team"; +import type { OsuMap } from "../models/OsuMap"; +import type { Score } from "../models/Score"; + +export interface RollResult { + teamId: string; + playerId: string; + roll: number; +} + +export interface ProtocolContext { + sendMessage: (message: string) => Promise; + setMap: (mapId: number) => Promise; + startGame: () => Promise; +} + +export interface IMatchProtocol { + readonly name: string; + readonly description: string; + + readonly teamSize: number; + readonly bestOf: number; + readonly hasPickBan: boolean; + readonly hasFreemod: boolean; + + onMatchStart(match: Match, context: ProtocolContext): Promise; + onAllPlayersJoined(match: Match, context: ProtocolContext): Promise; + onRollComplete( + match: Match, + rolls: RollResult[], + context: ProtocolContext + ): Promise; + onPick( + match: Match, + team: Team, + map: OsuMap, + context: ProtocolContext + ): Promise; + onBan( + match: Match, + team: Team, + map: OsuMap, + context: ProtocolContext + ): Promise; + onMapComplete( + match: Match, + scores: Score[], + context: ProtocolContext + ): Promise; + + isValidPick(match: Match, team: Team, map: OsuMap): boolean; + isValidBan(match: Match, team: Team, map: OsuMap): boolean; + + calculateMapWinner(match: Match, scores: Score[]): string; + calculateMatchWinner(match: Match): string | null; + + getPickingTeam(match: Match): Team; + getBanningTeam(match: Match): Team; + + getWinsNeeded(): number; +} diff --git a/backend/src/domain/protocols/ProtocolRegistry.ts b/backend/src/domain/protocols/ProtocolRegistry.ts new file mode 100644 index 0000000..e54aeef --- /dev/null +++ b/backend/src/domain/protocols/ProtocolRegistry.ts @@ -0,0 +1,32 @@ +import type { IMatchProtocol } from "./IMatchProtocol"; + +export class ProtocolRegistry { + private protocols: Map = new Map(); + + register(protocol: IMatchProtocol): void { + if (this.protocols.has(protocol.name)) { + throw new Error(`Protocol "${protocol.name}" is already registered`); + } + this.protocols.set(protocol.name, protocol); + } + + get(name: string): IMatchProtocol { + const protocol = this.protocols.get(name); + if (!protocol) { + throw new Error(`Protocol "${name}" not found`); + } + return protocol; + } + + has(name: string): boolean { + return this.protocols.has(name); + } + + list(): IMatchProtocol[] { + return Array.from(this.protocols.values()); + } + + names(): string[] { + return Array.from(this.protocols.keys()); + } +} diff --git a/backend/src/domain/protocols/StandardOsuProtocol.ts b/backend/src/domain/protocols/StandardOsuProtocol.ts new file mode 100644 index 0000000..36fa643 --- /dev/null +++ b/backend/src/domain/protocols/StandardOsuProtocol.ts @@ -0,0 +1,192 @@ +import type { + IMatchProtocol, + ProtocolContext, + RollResult, +} from "./IMatchProtocol"; +import type { Match } from "../models/Match"; +import { MatchState } from "../models/MatchState"; +import type { Team } from "../models/Team"; +import type { OsuMap } from "../models/OsuMap"; +import type { Score } from "../models/Score"; +import { getTeamWins } from "../models/Match"; + +export class StandardOsuProtocol implements IMatchProtocol { + readonly name = "standard-osu"; + readonly description = "Standard 4v4 tournament format with pick/ban"; + readonly teamSize = 4; + readonly bestOf = 9; + readonly hasPickBan = true; + readonly hasFreemod = true; + + async onMatchStart( + _match: Match, + context: ProtocolContext + ): Promise { + await context.sendMessage("Welcome to the match!"); + await context.sendMessage("Waiting for all players to join..."); + return MatchState.WAITING_PLAYERS; + } + + async onAllPlayersJoined( + _match: Match, + context: ProtocolContext + ): Promise { + await context.sendMessage("All players have joined! Please roll (!roll)"); + return MatchState.ROLLING; + } + + async onRollComplete( + match: Match, + rolls: RollResult[], + context: ProtocolContext + ): Promise { + const team1Roll = rolls.find((r) => r.teamId === match.teams[0].id); + const team2Roll = rolls.find((r) => r.teamId === match.teams[1].id); + + if (!team1Roll || !team2Roll) { + return null; + } + + const winningTeam = + team1Roll.roll > team2Roll.roll ? match.teams[0] : match.teams[1]; + const losingTeam = + team1Roll.roll > team2Roll.roll ? match.teams[1] : match.teams[0]; + + match.currentBanner = winningTeam.id; + match.currentPicker = losingTeam.id; + + await context.sendMessage( + `${winningTeam.name} wins the roll! They will ban first, ${losingTeam.name} picks first.` + ); + + return MatchState.BANNING; + } + + async onBan( + match: Match, + team: Team, + map: OsuMap, + context: ProtocolContext + ): Promise { + const existingBans = match.bans.get(team.id); + const bans = existingBans ? [...existingBans] : []; + bans.push(map.id); + match.bans.set(team.id, bans); + + await context.sendMessage(`${team.name} banned ${map.title} [${map.difficulty}]`); + + const totalBans = Array.from(match.bans.values()).flat().length; + if (totalBans >= 2) { + return MatchState.PICKING; + } + + const currentBanner = match.currentBanner; + match.currentBanner = + currentBanner === match.teams[0].id + ? match.teams[1].id + : match.teams[0].id; + + return null; + } + + async onPick( + match: Match, + team: Team, + map: OsuMap, + context: ProtocolContext + ): Promise { + match.picks.push(map.id); + await context.sendMessage(`${team.name} picked ${map.title} [${map.difficulty}]`); + await context.setMap(map.id); + + return MatchState.PLAYING; + } + + async onMapComplete( + match: Match, + _scores: Score[], + context: ProtocolContext + ): Promise { + const winner = this.calculateMatchWinner(match); + if (winner) { + return MatchState.COMPLETED; + } + + const currentPicker = match.currentPicker; + match.currentPicker = + currentPicker === match.teams[0].id + ? match.teams[1].id + : match.teams[0].id; + + const team1 = match.teams[0]; + const team2 = match.teams[1]; + await context.sendMessage( + `Score: ${team1.name} ${getTeamWins(match, team1.id)} - ${getTeamWins(match, team2.id)} ${team2.name}` + ); + + return MatchState.PICKING; + } + + isValidPick(match: Match, team: Team, map: OsuMap): boolean { + if (match.currentPicker !== team.id) return false; + + const allBans = Array.from(match.bans.values()).flat(); + if (allBans.includes(map.id)) return false; + + if (match.picks.includes(map.id)) return false; + + return true; + } + + isValidBan(match: Match, team: Team, map: OsuMap): boolean { + if (match.currentBanner !== team.id) return false; + + const allBans = Array.from(match.bans.values()).flat(); + if (allBans.includes(map.id)) return false; + + return true; + } + + calculateMapWinner(match: Match, scores: Score[]): string { + const team1 = match.teams[0]; + const team2 = match.teams[1]; + const team1PlayerIds = team1.players.map((p) => p.id); + const team2PlayerIds = team2.players.map((p) => p.id); + + const team1Score = scores + .filter((s) => team1PlayerIds.includes(s.playerId)) + .reduce((sum, s) => sum + s.score, 0); + + const team2Score = scores + .filter((s) => team2PlayerIds.includes(s.playerId)) + .reduce((sum, s) => sum + s.score, 0); + + return team1Score > team2Score ? team1.id : team2.id; + } + + calculateMatchWinner(match: Match): string | null { + const winsNeeded = this.getWinsNeeded(); + + for (const team of match.teams) { + if (getTeamWins(match, team.id) >= winsNeeded) { + return team.id; + } + } + + return null; + } + + getPickingTeam(match: Match): Team { + const team = match.teams.find((t) => t.id === match.currentPicker); + return team ?? match.teams[0]; + } + + getBanningTeam(match: Match): Team { + const team = match.teams.find((t) => t.id === match.currentBanner); + return team ?? match.teams[0]; + } + + getWinsNeeded(): number { + return Math.ceil(this.bestOf / 2); + } +} diff --git a/backend/src/domain/protocols/index.ts b/backend/src/domain/protocols/index.ts new file mode 100644 index 0000000..5b304d3 --- /dev/null +++ b/backend/src/domain/protocols/index.ts @@ -0,0 +1,4 @@ +export * from "./IMatchProtocol"; +export * from "./StandardOsuProtocol"; +export * from "./HeadToHeadProtocol"; +export * from "./ProtocolRegistry"; diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..04a9174 --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,197 @@ +import type { ServerWebSocket } from "bun"; +import { EventBus } from "./application/events/EventBus"; +import { MatchService } from "./application/services/MatchService"; +import { QueueService } from "./application/services/QueueService"; +import { ProtocolRegistry } from "./domain/protocols/ProtocolRegistry"; +import { StandardOsuProtocol } from "./domain/protocols/StandardOsuProtocol"; +import { HeadToHeadProtocol } from "./domain/protocols/HeadToHeadProtocol"; +import { InMemoryPlayerRepository } from "./infrastructure/mocks/InMemoryPlayerRepository"; +import { InMemoryTeamRepository } from "./infrastructure/mocks/InMemoryTeamRepository"; +import { InMemoryMatchRepository } from "./infrastructure/mocks/InMemoryMatchRepository"; +import { InMemoryQueueRepository } from "./infrastructure/mocks/InMemoryQueueRepository"; +import { MockGameService } from "./infrastructure/mocks/MockGameService"; +import { MockNotificationService } from "./infrastructure/mocks/MockNotificationService"; +import { WebSocketService } from "./infrastructure/websocket/WebSocketService"; +import { getTeamWins } from "./domain/models/Match"; +import { getAverageRating } from "./domain/models/Team"; + +interface WebSocketData { + id: string; + subscribedMatches: Set; +} + +const eventBus = new EventBus(); +const mockGameService = new MockGameService(); +const mockNotificationService = new MockNotificationService(); + +const playerRepo = new InMemoryPlayerRepository(); +const teamRepo = new InMemoryTeamRepository(); +const matchRepo = new InMemoryMatchRepository(); +const queueRepo = new InMemoryQueueRepository(); + +const protocolRegistry = new ProtocolRegistry(); +protocolRegistry.register(new StandardOsuProtocol()); +protocolRegistry.register(new HeadToHeadProtocol()); + +const matchService = new MatchService( + matchRepo, + playerRepo, + protocolRegistry, + mockGameService, + mockNotificationService, + eventBus +); + +const queueService = new QueueService(queueRepo, matchService, eventBus, { + minQueueTime: 0, + protocol: "head-to-head", +}); + +const wsService = new WebSocketService(eventBus); + +const server = Bun.serve({ + port: process.env.PORT ? parseInt(process.env.PORT) : 3000, + + async fetch(req, server) { + const url = new URL(req.url); + + if (url.pathname === "/ws") { + const upgraded = server.upgrade(req, { + data: { + id: crypto.randomUUID(), + subscribedMatches: new Set(), + }, + }); + if (upgraded) return undefined; + return new Response("WebSocket upgrade failed", { status: 400 }); + } + + if (url.pathname === "/api/health") { + return Response.json({ status: "ok", timestamp: new Date().toISOString() }); + } + + if (url.pathname === "/api/queue/status" && req.method === "GET") { + const queueType = url.searchParams.get("type") ?? "ranked"; + const status = await queueService.getQueueStatus(queueType); + return Response.json(status); + } + + if (url.pathname === "/api/queue/join" && req.method === "POST") { + try { + const body = await req.json(); + const team = await teamRepo.findById(body.teamId); + if (!team) { + return Response.json({ error: "Team not found" }, { status: 404 }); + } + const entry = await queueService.joinQueue(team, body.queueType ?? "ranked"); + return Response.json({ success: true, entry: { id: entry.id, joinedAt: entry.joinedAt } }); + } catch (error) { + return Response.json({ error: (error as Error).message }, { status: 400 }); + } + } + + if (url.pathname === "/api/queue/leave" && req.method === "POST") { + try { + const body = await req.json(); + await queueService.leaveQueue(body.teamId); + return Response.json({ success: true }); + } catch (error) { + return Response.json({ error: (error as Error).message }, { status: 400 }); + } + } + + if (url.pathname.startsWith("/api/matches/") && req.method === "GET") { + const matchId = url.pathname.split("/").pop(); + if (!matchId) { + return Response.json({ error: "Match ID required" }, { status: 400 }); + } + const match = await matchRepo.findById(matchId); + if (!match) { + return Response.json({ error: "Match not found" }, { status: 404 }); + } + return Response.json({ + id: match.id, + state: match.state, + protocol: match.protocol, + teams: match.teams.map((t) => ({ + id: t.id, + name: t.name, + players: t.players.map((p) => ({ + id: p.id, + displayName: p.displayName, + rating: p.rating, + })), + averageRating: getAverageRating(t), + wins: getTeamWins(match, t.id), + })), + winner: match.winner ? { id: match.winner.id, name: match.winner.name } : null, + createdAt: match.createdAt, + startedAt: match.startedAt, + endedAt: match.endedAt, + }); + } + + if (url.pathname === "/api/matches" && req.method === "GET") { + const matches = await matchRepo.findAll(); + return Response.json( + matches.map((m) => ({ + id: m.id, + state: m.state, + teams: m.teams.map((t) => t.name), + winner: m.winner?.name ?? null, + createdAt: m.createdAt, + })) + ); + } + + if (url.pathname === "/api/players" && req.method === "GET") { + const players = await playerRepo.findAll(); + return Response.json(players); + } + + if (url.pathname === "/api/teams" && req.method === "GET") { + const teams = await teamRepo.findAll(); + return Response.json( + teams.map((t) => ({ + id: t.id, + name: t.name, + playerCount: t.players.length, + averageRating: getAverageRating(t), + })) + ); + } + + if (url.pathname === "/api/protocols" && req.method === "GET") { + const protocols = protocolRegistry.list(); + return Response.json( + protocols.map((p) => ({ + name: p.name, + description: p.description, + teamSize: p.teamSize, + bestOf: p.bestOf, + hasPickBan: p.hasPickBan, + })) + ); + } + + return new Response("Not Found", { status: 404 }); + }, + + websocket: { + open(ws: ServerWebSocket) { + wsService.handleOpen(ws); + }, + message(ws: ServerWebSocket, message: string | Buffer) { + wsService.handleMessage(ws, message); + }, + close(ws: ServerWebSocket) { + wsService.handleClose(ws); + }, + }, +}); + +console.log(`Server running at http://localhost:${server.port}`); +console.log(`WebSocket available at ws://localhost:${server.port}/ws`); + +queueService.startAutoMatchmaking(5000); +console.log("Auto-matchmaking started (5s interval)"); diff --git a/backend/src/infrastructure/database/PostgresMatchRepository.ts b/backend/src/infrastructure/database/PostgresMatchRepository.ts new file mode 100644 index 0000000..27a0efd --- /dev/null +++ b/backend/src/infrastructure/database/PostgresMatchRepository.ts @@ -0,0 +1,293 @@ +import type { SQL } from "bun"; +import type { IMatchRepository } from "../../domain/interfaces/IMatchRepository"; +import type { Match } from "../../domain/models/Match"; +import { MatchState, isActiveState } from "../../domain/models/MatchState"; +import type { Team } from "../../domain/models/Team"; +import type { Player } from "../../domain/models/Player"; +import type { MatchScore, Score } from "../../domain/models/Score"; + +interface MatchRow { + id: string; + protocol: string; + state: string; + lobby_id: string | null; + winner_team_id: string | null; + created_at: Date; + started_at: Date | null; + ended_at: Date | null; +} + +interface PlayerRow { + id: string; + discord_id: string; + osu_id: number; + osu_name: string; + display_name: string; + rating: number; + created_at: Date; +} + +interface TeamRow { + id: string; + name: string; + captain_id: string; + created_at: Date; +} + +export class PostgresMatchRepository implements IMatchRepository { + constructor(private db: SQL) {} + + private rowToPlayer(row: PlayerRow): Player { + return { + id: row.id, + discordId: row.discord_id, + osuId: row.osu_id, + osuName: row.osu_name, + displayName: row.display_name, + rating: row.rating, + createdAt: row.created_at, + }; + } + + private async loadTeamPlayers(teamId: string): Promise { + const rows = await this.db` + SELECT p.* FROM players p + JOIN team_players tp ON tp.player_id = p.id + WHERE tp.team_id = ${teamId} + `; + return rows.map((row) => this.rowToPlayer(row as PlayerRow)); + } + + private async loadMatchTeams(matchId: string): Promise { + const teamRows = await this.db` + SELECT t.*, mt.team_order FROM teams t + JOIN match_teams mt ON mt.team_id = t.id + WHERE mt.match_id = ${matchId} + ORDER BY mt.team_order + `; + + const teams: Team[] = []; + for (const row of teamRows as (TeamRow & { team_order: number })[]) { + const players = await this.loadTeamPlayers(row.id); + const captain = players.find((p) => p.id === row.captain_id) ?? players[0]; + teams.push({ + id: row.id, + name: row.name, + players, + captain, + createdAt: row.created_at, + }); + } + + return teams; + } + + private async loadMatchScores(matchId: string): Promise { + const scoreRows = await this.db` + SELECT * FROM match_scores WHERE match_id = ${matchId} ORDER BY map_order + `; + + const scores: MatchScore[] = []; + for (const row of scoreRows as any[]) { + const playerScoreRows = await this.db` + SELECT * FROM player_scores WHERE match_score_id = ${row.id} + `; + + const teamScores = new Map(); + const playerScores = new Map(); + + for (const ps of playerScoreRows as any[]) { + playerScores.set(ps.player_id, { + playerId: ps.player_id, + score: ps.score, + accuracy: ps.accuracy, + maxCombo: ps.max_combo, + misses: ps.misses, + mods: ps.mods, + }); + } + + scores.push({ + id: row.id, + mapId: row.map_id, + teamScores, + playerScores, + winner: row.winner_team_id, + }); + } + + return scores; + } + + private async loadMatchRolls(matchId: string): Promise> { + const rows = await this.db`SELECT * FROM match_rolls WHERE match_id = ${matchId}`; + const rolls = new Map(); + for (const row of rows as any[]) { + rolls.set(row.team_id, row.roll_value); + } + return rolls; + } + + private async loadMatchBans(matchId: string): Promise> { + const rows = await this.db` + SELECT * FROM match_bans WHERE match_id = ${matchId} ORDER BY ban_order + `; + const bans = new Map(); + for (const row of rows as any[]) { + const teamBans = bans.get(row.team_id) ?? []; + teamBans.push(row.map_id); + bans.set(row.team_id, teamBans); + } + return bans; + } + + private async loadMatchPicks(matchId: string): Promise { + const rows = await this.db` + SELECT * FROM match_picks WHERE match_id = ${matchId} ORDER BY pick_order + `; + return (rows as any[]).map((r) => r.map_id); + } + + async findById(id: string): Promise { + const rows = await this.db`SELECT * FROM matches WHERE id = ${id}`; + if (rows.length === 0) return null; + + const row = rows[0] as MatchRow; + const teams = await this.loadMatchTeams(id); + const scores = await this.loadMatchScores(id); + const rolls = await this.loadMatchRolls(id); + const bans = await this.loadMatchBans(id); + const picks = await this.loadMatchPicks(id); + + const winner = row.winner_team_id + ? teams.find((t) => t.id === row.winner_team_id) + : undefined; + + return { + id: row.id, + teams, + protocol: row.protocol, + state: row.state as MatchState, + scores, + winner, + lobbyId: row.lobby_id ?? undefined, + createdAt: row.created_at, + startedAt: row.started_at ?? undefined, + endedAt: row.ended_at ?? undefined, + rolls, + bans, + picks, + joinedPlayers: new Set(), + currentPicker: undefined, + currentBanner: undefined, + }; + } + + async findActive(): Promise { + const rows = await this.db` + SELECT id FROM matches WHERE state NOT IN ('completed', 'cancelled') + `; + const matches: Match[] = []; + for (const row of rows as { id: string }[]) { + const match = await this.findById(row.id); + if (match) matches.push(match); + } + return matches; + } + + async findByPlayer(playerId: string, limit?: number): Promise { + const query = limit + ? this.db` + SELECT DISTINCT m.id FROM matches m + JOIN match_teams mt ON mt.match_id = m.id + JOIN team_players tp ON tp.team_id = mt.team_id + WHERE tp.player_id = ${playerId} + ORDER BY m.created_at DESC + LIMIT ${limit} + ` + : this.db` + SELECT DISTINCT m.id FROM matches m + JOIN match_teams mt ON mt.match_id = m.id + JOIN team_players tp ON tp.team_id = mt.team_id + WHERE tp.player_id = ${playerId} + ORDER BY m.created_at DESC + `; + + const rows = await query; + const matches: Match[] = []; + for (const row of rows as { id: string }[]) { + const match = await this.findById(row.id); + if (match) matches.push(match); + } + return matches; + } + + async findByLobbyId(lobbyId: string): Promise { + const rows = await this.db`SELECT id FROM matches WHERE lobby_id = ${lobbyId}`; + if (rows.length === 0) return null; + return this.findById((rows[0] as { id: string }).id); + } + + async save(match: Match): Promise { + await this.db` + INSERT INTO matches (id, protocol, state, lobby_id, winner_team_id, created_at, started_at, ended_at) + VALUES (${match.id}, ${match.protocol}, ${match.state}, ${match.lobbyId ?? null}, ${match.winner?.id ?? null}, ${match.createdAt}, ${match.startedAt ?? null}, ${match.endedAt ?? null}) + ON CONFLICT (id) DO UPDATE SET + protocol = EXCLUDED.protocol, + state = EXCLUDED.state, + lobby_id = EXCLUDED.lobby_id, + winner_team_id = EXCLUDED.winner_team_id, + started_at = EXCLUDED.started_at, + ended_at = EXCLUDED.ended_at + `; + + await this.db`DELETE FROM match_teams WHERE match_id = ${match.id}`; + for (let i = 0; i < match.teams.length; i++) { + await this.db` + INSERT INTO match_teams (match_id, team_id, team_order) + VALUES (${match.id}, ${match.teams[i].id}, ${i}) + `; + } + + await this.db`DELETE FROM match_rolls WHERE match_id = ${match.id}`; + for (const [teamId, roll] of match.rolls) { + await this.db` + INSERT INTO match_rolls (match_id, team_id, roll_value) + VALUES (${match.id}, ${teamId}, ${roll}) + `; + } + + await this.db`DELETE FROM match_bans WHERE match_id = ${match.id}`; + let banOrder = 0; + for (const [teamId, mapIds] of match.bans) { + for (const mapId of mapIds) { + await this.db` + INSERT INTO match_bans (match_id, team_id, map_id, ban_order) + VALUES (${match.id}, ${teamId}, ${mapId}, ${banOrder++}) + `; + } + } + + await this.db`DELETE FROM match_picks WHERE match_id = ${match.id}`; + for (let i = 0; i < match.picks.length; i++) { + await this.db` + INSERT INTO match_picks (match_id, map_id, pick_order) + VALUES (${match.id}, ${match.picks[i]}, ${i}) + `; + } + } + + async updateState(matchId: string, state: MatchState): Promise { + await this.db`UPDATE matches SET state = ${state} WHERE id = ${matchId}`; + } + + async findAll(): Promise { + const rows = await this.db`SELECT id FROM matches ORDER BY created_at DESC`; + const matches: Match[] = []; + for (const row of rows as { id: string }[]) { + const match = await this.findById(row.id); + if (match) matches.push(match); + } + return matches; + } +} diff --git a/backend/src/infrastructure/database/PostgresPlayerRepository.ts b/backend/src/infrastructure/database/PostgresPlayerRepository.ts new file mode 100644 index 0000000..e9122c4 --- /dev/null +++ b/backend/src/infrastructure/database/PostgresPlayerRepository.ts @@ -0,0 +1,77 @@ +import type { SQL } from "bun"; +import type { IPlayerRepository } from "../../domain/interfaces/IPlayerRepository"; +import type { Player } from "../../domain/models/Player"; + +interface PlayerRow { + id: string; + discord_id: string; + osu_id: number; + osu_name: string; + display_name: string; + rating: number; + created_at: Date; +} + +export class PostgresPlayerRepository implements IPlayerRepository { + constructor(private db: SQL) {} + + private rowToPlayer(row: PlayerRow): Player { + return { + id: row.id, + discordId: row.discord_id, + osuId: row.osu_id, + osuName: row.osu_name, + displayName: row.display_name, + rating: row.rating, + createdAt: row.created_at, + }; + } + + async findById(id: string): Promise { + const rows = await this.db` + SELECT * FROM players WHERE id = ${id} + `; + if (rows.length === 0) return null; + return this.rowToPlayer(rows[0] as PlayerRow); + } + + async findByOsuId(osuId: number): Promise { + const rows = await this.db` + SELECT * FROM players WHERE osu_id = ${osuId} + `; + if (rows.length === 0) return null; + return this.rowToPlayer(rows[0] as PlayerRow); + } + + async findByDiscordId(discordId: string): Promise { + const rows = await this.db` + SELECT * FROM players WHERE discord_id = ${discordId} + `; + if (rows.length === 0) return null; + return this.rowToPlayer(rows[0] as PlayerRow); + } + + async save(player: Player): Promise { + await this.db` + INSERT INTO players (id, discord_id, osu_id, osu_name, display_name, rating, created_at) + VALUES (${player.id}, ${player.discordId}, ${player.osuId}, ${player.osuName}, ${player.displayName}, ${player.rating}, ${player.createdAt}) + ON CONFLICT (id) DO UPDATE SET + discord_id = EXCLUDED.discord_id, + osu_id = EXCLUDED.osu_id, + osu_name = EXCLUDED.osu_name, + display_name = EXCLUDED.display_name, + rating = EXCLUDED.rating + `; + } + + async updateRating(playerId: string, newRating: number): Promise { + await this.db` + UPDATE players SET rating = ${newRating} WHERE id = ${playerId} + `; + } + + async findAll(): Promise { + const rows = await this.db`SELECT * FROM players ORDER BY created_at DESC`; + return rows.map((row) => this.rowToPlayer(row as PlayerRow)); + } +} diff --git a/backend/src/infrastructure/database/PostgresQueueRepository.ts b/backend/src/infrastructure/database/PostgresQueueRepository.ts new file mode 100644 index 0000000..b08b81e --- /dev/null +++ b/backend/src/infrastructure/database/PostgresQueueRepository.ts @@ -0,0 +1,173 @@ +import type { SQL } from "bun"; +import type { IQueueRepository } from "../../domain/interfaces/IQueueRepository"; +import type { QueueEntry } from "../../domain/models/QueueEntry"; +import type { Team } from "../../domain/models/Team"; +import type { Player } from "../../domain/models/Player"; +import { getAverageRating } from "../../domain/models/Team"; + +interface PlayerRow { + id: string; + discord_id: string; + osu_id: number; + osu_name: string; + display_name: string; + rating: number; + created_at: Date; +} + +interface TeamRow { + id: string; + name: string; + captain_id: string; + created_at: Date; +} + +interface QueueEntryRow { + id: string; + team_id: string; + queue_type: string; + joined_at: Date; +} + +export class PostgresQueueRepository implements IQueueRepository { + constructor(private db: SQL) {} + + private rowToPlayer(row: PlayerRow): Player { + return { + id: row.id, + discordId: row.discord_id, + osuId: row.osu_id, + osuName: row.osu_name, + displayName: row.display_name, + rating: row.rating, + createdAt: row.created_at, + }; + } + + private async loadTeamPlayers(teamId: string): Promise { + const rows = await this.db` + SELECT p.* FROM players p + JOIN team_players tp ON tp.player_id = p.id + WHERE tp.team_id = ${teamId} + `; + return rows.map((row) => this.rowToPlayer(row as PlayerRow)); + } + + private async loadTeam(teamId: string): Promise { + const rows = await this.db`SELECT * FROM teams WHERE id = ${teamId}`; + if (rows.length === 0) return null; + + const row = rows[0] as TeamRow; + const players = await this.loadTeamPlayers(teamId); + const captain = players.find((p) => p.id === row.captain_id) ?? players[0]; + + return { + id: row.id, + name: row.name, + players, + captain, + createdAt: row.created_at, + }; + } + + async add(entry: QueueEntry): Promise { + await this.db` + INSERT INTO queue_entries (id, team_id, queue_type, joined_at) + VALUES (${entry.id}, ${entry.team.id}, ${entry.queueType}, ${entry.joinedAt}) + ON CONFLICT (team_id) DO UPDATE SET + queue_type = EXCLUDED.queue_type, + joined_at = EXCLUDED.joined_at + `; + } + + async remove(teamId: string): Promise { + await this.db`DELETE FROM queue_entries WHERE team_id = ${teamId}`; + } + + async findByTeamId(teamId: string): Promise { + const rows = await this.db`SELECT * FROM queue_entries WHERE team_id = ${teamId}`; + if (rows.length === 0) return null; + + const row = rows[0] as QueueEntryRow; + const team = await this.loadTeam(row.team_id); + if (!team) return null; + + return { + id: row.id, + team, + queueType: row.queue_type, + joinedAt: row.joined_at, + }; + } + + async findByType(queueType: string): Promise { + const rows = await this.db` + SELECT * FROM queue_entries + WHERE queue_type = ${queueType} + ORDER BY joined_at ASC + `; + + const entries: QueueEntry[] = []; + for (const row of rows as QueueEntryRow[]) { + const team = await this.loadTeam(row.team_id); + if (team) { + entries.push({ + id: row.id, + team, + queueType: row.queue_type, + joinedAt: row.joined_at, + }); + } + } + + return entries; + } + + async findClosestByRating( + rating: number, + queueType: string, + exclude?: string[] + ): Promise { + const entries = await this.findByType(queueType); + + let closest: QueueEntry | null = null; + let closestDiff = Infinity; + + for (const entry of entries) { + if (exclude?.includes(entry.team.id)) continue; + + const teamRating = getAverageRating(entry.team); + const diff = Math.abs(teamRating - rating); + + if (diff < closestDiff) { + closestDiff = diff; + closest = entry; + } + } + + return closest; + } + + async findAll(): Promise { + const rows = await this.db`SELECT * FROM queue_entries ORDER BY joined_at ASC`; + + const entries: QueueEntry[] = []; + for (const row of rows as QueueEntryRow[]) { + const team = await this.loadTeam(row.team_id); + if (team) { + entries.push({ + id: row.id, + team, + queueType: row.queue_type, + joinedAt: row.joined_at, + }); + } + } + + return entries; + } + + async clear(): Promise { + await this.db`DELETE FROM queue_entries`; + } +} diff --git a/backend/src/infrastructure/database/PostgresTeamRepository.ts b/backend/src/infrastructure/database/PostgresTeamRepository.ts new file mode 100644 index 0000000..abd10ef --- /dev/null +++ b/backend/src/infrastructure/database/PostgresTeamRepository.ts @@ -0,0 +1,128 @@ +import type { SQL } from "bun"; +import type { ITeamRepository } from "../../domain/interfaces/ITeamRepository"; +import type { Team } from "../../domain/models/Team"; +import type { Player } from "../../domain/models/Player"; + +interface TeamRow { + id: string; + name: string; + captain_id: string; + created_at: Date; +} + +interface PlayerRow { + id: string; + discord_id: string; + osu_id: number; + osu_name: string; + display_name: string; + rating: number; + created_at: Date; +} + +export class PostgresTeamRepository implements ITeamRepository { + constructor(private db: SQL) {} + + private rowToPlayer(row: PlayerRow): Player { + return { + id: row.id, + discordId: row.discord_id, + osuId: row.osu_id, + osuName: row.osu_name, + displayName: row.display_name, + rating: row.rating, + createdAt: row.created_at, + }; + } + + private async loadTeamPlayers(teamId: string): Promise { + const rows = await this.db` + SELECT p.* FROM players p + JOIN team_players tp ON tp.player_id = p.id + WHERE tp.team_id = ${teamId} + `; + return rows.map((row) => this.rowToPlayer(row as PlayerRow)); + } + + async findById(id: string): Promise { + const rows = await this.db`SELECT * FROM teams WHERE id = ${id}`; + if (rows.length === 0) return null; + + const row = rows[0] as TeamRow; + const players = await this.loadTeamPlayers(id); + const captain = players.find((p) => p.id === row.captain_id) ?? players[0]; + + return { + id: row.id, + name: row.name, + players, + captain, + createdAt: row.created_at, + }; + } + + async findByPlayer(playerId: string): Promise { + const teamRows = await this.db` + SELECT t.* FROM teams t + JOIN team_players tp ON tp.team_id = t.id + WHERE tp.player_id = ${playerId} + `; + + const teams: Team[] = []; + for (const row of teamRows as TeamRow[]) { + const players = await this.loadTeamPlayers(row.id); + const captain = players.find((p) => p.id === row.captain_id) ?? players[0]; + teams.push({ + id: row.id, + name: row.name, + players, + captain, + createdAt: row.created_at, + }); + } + + return teams; + } + + async save(team: Team): Promise { + await this.db` + INSERT INTO teams (id, name, captain_id, created_at) + VALUES (${team.id}, ${team.name}, ${team.captain.id}, ${team.createdAt}) + ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + captain_id = EXCLUDED.captain_id + `; + + await this.db`DELETE FROM team_players WHERE team_id = ${team.id}`; + + for (const player of team.players) { + await this.db` + INSERT INTO team_players (team_id, player_id) + VALUES (${team.id}, ${player.id}) + `; + } + } + + async delete(teamId: string): Promise { + await this.db`DELETE FROM teams WHERE id = ${teamId}`; + } + + async findAll(): Promise { + const teamRows = await this.db`SELECT * FROM teams ORDER BY created_at DESC`; + const teams: Team[] = []; + + for (const row of teamRows as TeamRow[]) { + const players = await this.loadTeamPlayers(row.id); + const captain = players.find((p) => p.id === row.captain_id) ?? players[0]; + teams.push({ + id: row.id, + name: row.name, + players, + captain, + createdAt: row.created_at, + }); + } + + return teams; + } +} diff --git a/backend/src/infrastructure/database/index.ts b/backend/src/infrastructure/database/index.ts new file mode 100644 index 0000000..b90cb54 --- /dev/null +++ b/backend/src/infrastructure/database/index.ts @@ -0,0 +1,4 @@ +export * from "./PostgresPlayerRepository"; +export * from "./PostgresTeamRepository"; +export * from "./PostgresMatchRepository"; +export * from "./PostgresQueueRepository"; diff --git a/backend/src/infrastructure/database/migrations/001_initial.sql b/backend/src/infrastructure/database/migrations/001_initial.sql new file mode 100644 index 0000000..109a6bf --- /dev/null +++ b/backend/src/infrastructure/database/migrations/001_initial.sql @@ -0,0 +1,98 @@ +-- Initial database schema for Vash Esports Backend + +CREATE TABLE IF NOT EXISTS players ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + discord_id VARCHAR(255) UNIQUE NOT NULL, + osu_id INTEGER UNIQUE NOT NULL, + osu_name VARCHAR(255) NOT NULL, + display_name VARCHAR(255) NOT NULL, + rating INTEGER DEFAULT 1000, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS teams ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + captain_id UUID REFERENCES players(id), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS team_players ( + team_id UUID REFERENCES teams(id) ON DELETE CASCADE, + player_id UUID REFERENCES players(id) ON DELETE CASCADE, + PRIMARY KEY (team_id, player_id) +); + +CREATE TABLE IF NOT EXISTS matches ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + protocol VARCHAR(100) NOT NULL, + state VARCHAR(50) NOT NULL, + lobby_id VARCHAR(255), + winner_team_id UUID REFERENCES teams(id), + created_at TIMESTAMP DEFAULT NOW(), + started_at TIMESTAMP, + ended_at TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS match_teams ( + match_id UUID REFERENCES matches(id) ON DELETE CASCADE, + team_id UUID REFERENCES teams(id) ON DELETE CASCADE, + team_order INTEGER NOT NULL, + PRIMARY KEY (match_id, team_id) +); + +CREATE TABLE IF NOT EXISTS match_rolls ( + match_id UUID REFERENCES matches(id) ON DELETE CASCADE, + team_id UUID REFERENCES teams(id) ON DELETE CASCADE, + roll_value INTEGER NOT NULL, + PRIMARY KEY (match_id, team_id) +); + +CREATE TABLE IF NOT EXISTS match_bans ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + match_id UUID REFERENCES matches(id) ON DELETE CASCADE, + team_id UUID REFERENCES teams(id) ON DELETE CASCADE, + map_id INTEGER NOT NULL, + ban_order INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS match_picks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + match_id UUID REFERENCES matches(id) ON DELETE CASCADE, + map_id INTEGER NOT NULL, + pick_order INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS match_scores ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + match_id UUID REFERENCES matches(id) ON DELETE CASCADE, + map_id INTEGER NOT NULL, + winner_team_id UUID REFERENCES teams(id), + map_order INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS player_scores ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + match_score_id UUID REFERENCES match_scores(id) ON DELETE CASCADE, + player_id UUID REFERENCES players(id), + score INTEGER NOT NULL, + accuracy DECIMAL(5,2), + max_combo INTEGER, + misses INTEGER, + mods TEXT[] +); + +CREATE TABLE IF NOT EXISTS queue_entries ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + team_id UUID REFERENCES teams(id) ON DELETE CASCADE UNIQUE, + queue_type VARCHAR(50) NOT NULL, + joined_at TIMESTAMP DEFAULT NOW() +); + +-- Indexes for common queries +CREATE INDEX IF NOT EXISTS idx_players_osu_id ON players(osu_id); +CREATE INDEX IF NOT EXISTS idx_players_discord_id ON players(discord_id); +CREATE INDEX IF NOT EXISTS idx_matches_state ON matches(state) WHERE state NOT IN ('completed', 'cancelled'); +CREATE INDEX IF NOT EXISTS idx_matches_lobby_id ON matches(lobby_id) WHERE lobby_id IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_queue_entries_type ON queue_entries(queue_type, joined_at); +CREATE INDEX IF NOT EXISTS idx_team_players_player ON team_players(player_id); diff --git a/backend/src/infrastructure/discord/DiscordService.ts b/backend/src/infrastructure/discord/DiscordService.ts new file mode 100644 index 0000000..3f1644a --- /dev/null +++ b/backend/src/infrastructure/discord/DiscordService.ts @@ -0,0 +1,174 @@ +import { Client, GatewayIntentBits, EmbedBuilder, type TextChannel } from "discord.js"; +import type { INotificationService } from "../../domain/interfaces/INotificationService"; +import type { Match } from "../../domain/models/Match"; +import type { Team } from "../../domain/models/Team"; +import type { MatchScore } from "../../domain/models/Score"; +import type { EventBus } from "../../application/events/EventBus"; +import { getTeamWins } from "../../domain/models/Match"; + +export interface DiscordConfig { + token: string; + guildId: string; + announcementChannelId: string; + matchResultsChannelId: string; +} + +export class DiscordService implements INotificationService { + private client: Client; + private ready = false; + + constructor( + private config: DiscordConfig, + private eventBus?: EventBus + ) { + this.client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], + }); + + this.setupClient(); + } + + private async setupClient(): Promise { + this.client.on("ready", () => { + console.log(`[DiscordService] Logged in as ${this.client.user?.tag}`); + this.ready = true; + }); + + this.client.on("error", (error) => { + console.error("[DiscordService] Client error:", error); + }); + + if (this.config.token) { + try { + await this.client.login(this.config.token); + } catch (error) { + console.error("[DiscordService] Failed to login:", error); + } + } + } + + async announceMatch(match: Match): Promise { + if (!this.ready) { + console.log("[DiscordService] Not ready, skipping announcement"); + return; + } + + const channel = await this.client.channels.fetch( + this.config.announcementChannelId + ); + if (!channel || !channel.isTextBased()) return; + + const embed = new EmbedBuilder() + .setTitle("New Match Starting!") + .setDescription( + `**${match.teams[0].name}** vs **${match.teams[1].name}**` + ) + .addFields( + { + name: match.teams[0].name, + value: match.teams[0].players.map((p) => p.displayName).join("\n"), + inline: true, + }, + { + name: match.teams[1].name, + value: match.teams[1].players.map((p) => p.displayName).join("\n"), + inline: true, + } + ) + .setColor(0x00ae86) + .setTimestamp(); + + await (channel as TextChannel).send({ embeds: [embed] }); + } + + async announceMatchResult(match: Match, winner: Team): Promise { + if (!this.ready) { + console.log("[DiscordService] Not ready, skipping result announcement"); + return; + } + + const channel = await this.client.channels.fetch( + this.config.matchResultsChannelId + ); + if (!channel || !channel.isTextBased()) return; + + const team1Wins = getTeamWins(match, match.teams[0].id); + const team2Wins = getTeamWins(match, match.teams[1].id); + + const embed = new EmbedBuilder() + .setTitle("Match Completed!") + .setDescription(`**${winner.name}** wins!`) + .addFields({ + name: "Final Score", + value: `${match.teams[0].name} **${team1Wins}** - **${team2Wins}** ${match.teams[1].name}`, + }) + .setColor(winner.id === match.teams[0].id ? 0x3498db : 0xe74c3c) + .setTimestamp(); + + await (channel as TextChannel).send({ embeds: [embed] }); + } + + async notifyPlayer(playerId: string, message: string): Promise { + if (!this.ready) { + console.log("[DiscordService] Not ready, skipping player notification"); + return; + } + + try { + const user = await this.client.users.fetch(playerId); + await user.send(message); + } catch (error) { + console.error(`[DiscordService] Failed to DM player ${playerId}:`, error); + } + } + + async notifyTeam(teamId: string, message: string): Promise { + // In a real implementation, you'd look up the team's players + // and notify each one. For now, just log. + console.log(`[DiscordService] Team ${teamId}: ${message}`); + } + + async announceMapResult(match: Match, scores: MatchScore): Promise { + if (!this.ready) { + console.log("[DiscordService] Not ready, skipping map result"); + return; + } + + const channel = await this.client.channels.fetch( + this.config.announcementChannelId + ); + if (!channel || !channel.isTextBased()) return; + + const winner = match.teams.find((t) => t.id === scores.winner); + const team1Score = scores.teamScores.get(match.teams[0].id) ?? 0; + const team2Score = scores.teamScores.get(match.teams[1].id) ?? 0; + const team1Wins = getTeamWins(match, match.teams[0].id); + const team2Wins = getTeamWins(match, match.teams[1].id); + + const embed = new EmbedBuilder() + .setTitle("Map Complete") + .setDescription(`**${winner?.name}** wins the map!`) + .addFields( + { + name: "Map Score", + value: `${match.teams[0].name}: ${team1Score.toLocaleString()}\n${match.teams[1].name}: ${team2Score.toLocaleString()}`, + }, + { + name: "Match Score", + value: `${match.teams[0].name} **${team1Wins}** - **${team2Wins}** ${match.teams[1].name}`, + } + ) + .setColor(0x9b59b6) + .setTimestamp(); + + await (channel as TextChannel).send({ embeds: [embed] }); + } + + async destroy(): Promise { + await this.client.destroy(); + } +} diff --git a/backend/src/infrastructure/discord/index.ts b/backend/src/infrastructure/discord/index.ts new file mode 100644 index 0000000..dbedea0 --- /dev/null +++ b/backend/src/infrastructure/discord/index.ts @@ -0,0 +1 @@ +export * from "./DiscordService"; diff --git a/backend/src/infrastructure/mocks/InMemoryMatchRepository.ts b/backend/src/infrastructure/mocks/InMemoryMatchRepository.ts new file mode 100644 index 0000000..f95a5f6 --- /dev/null +++ b/backend/src/infrastructure/mocks/InMemoryMatchRepository.ts @@ -0,0 +1,63 @@ +import type { IMatchRepository } from "../../domain/interfaces/IMatchRepository"; +import type { Match } from "../../domain/models/Match"; +import { MatchState, isActiveState } from "../../domain/models/MatchState"; + +export class InMemoryMatchRepository implements IMatchRepository { + private matches: Map = new Map(); + + async findById(id: string): Promise { + return this.matches.get(id) ?? null; + } + + async findActive(): Promise { + const result: Match[] = []; + for (const match of this.matches.values()) { + if (isActiveState(match.state)) { + result.push(match); + } + } + return result; + } + + async findByPlayer(playerId: string, limit?: number): Promise { + const result: Match[] = []; + for (const match of this.matches.values()) { + const hasPlayer = match.teams.some((t) => + t.players.some((p) => p.id === playerId) + ); + if (hasPlayer) { + result.push(match); + if (limit && result.length >= limit) break; + } + } + return result; + } + + async findByLobbyId(lobbyId: string): Promise { + for (const match of this.matches.values()) { + if (match.lobbyId === lobbyId) { + return match; + } + } + return null; + } + + async save(match: Match): Promise { + this.matches.set(match.id, match); + } + + async updateState(matchId: string, state: MatchState): Promise { + const match = this.matches.get(matchId); + if (match) { + match.state = state; + } + } + + async findAll(): Promise { + return Array.from(this.matches.values()); + } + + clear(): void { + this.matches.clear(); + } +} diff --git a/backend/src/infrastructure/mocks/InMemoryPlayerRepository.ts b/backend/src/infrastructure/mocks/InMemoryPlayerRepository.ts new file mode 100644 index 0000000..138df03 --- /dev/null +++ b/backend/src/infrastructure/mocks/InMemoryPlayerRepository.ts @@ -0,0 +1,47 @@ +import type { IPlayerRepository } from "../../domain/interfaces/IPlayerRepository"; +import type { Player } from "../../domain/models/Player"; + +export class InMemoryPlayerRepository implements IPlayerRepository { + private players: Map = new Map(); + + async findById(id: string): Promise { + return this.players.get(id) ?? null; + } + + async findByOsuId(osuId: number): Promise { + for (const player of this.players.values()) { + if (player.osuId === osuId) { + return player; + } + } + return null; + } + + async findByDiscordId(discordId: string): Promise { + for (const player of this.players.values()) { + if (player.discordId === discordId) { + return player; + } + } + return null; + } + + async save(player: Player): Promise { + this.players.set(player.id, player); + } + + async updateRating(playerId: string, newRating: number): Promise { + const player = this.players.get(playerId); + if (player) { + player.rating = newRating; + } + } + + async findAll(): Promise { + return Array.from(this.players.values()); + } + + clear(): void { + this.players.clear(); + } +} diff --git a/backend/src/infrastructure/mocks/InMemoryQueueRepository.ts b/backend/src/infrastructure/mocks/InMemoryQueueRepository.ts new file mode 100644 index 0000000..af402b9 --- /dev/null +++ b/backend/src/infrastructure/mocks/InMemoryQueueRepository.ts @@ -0,0 +1,61 @@ +import type { IQueueRepository } from "../../domain/interfaces/IQueueRepository"; +import type { QueueEntry } from "../../domain/models/QueueEntry"; +import { getAverageRating } from "../../domain/models/Team"; + +export class InMemoryQueueRepository implements IQueueRepository { + private entries: Map = new Map(); + + async add(entry: QueueEntry): Promise { + this.entries.set(entry.team.id, entry); + } + + async remove(teamId: string): Promise { + this.entries.delete(teamId); + } + + async findByTeamId(teamId: string): Promise { + return this.entries.get(teamId) ?? null; + } + + async findByType(queueType: string): Promise { + const result: QueueEntry[] = []; + for (const entry of this.entries.values()) { + if (entry.queueType === queueType) { + result.push(entry); + } + } + return result.sort((a, b) => a.joinedAt.getTime() - b.joinedAt.getTime()); + } + + async findClosestByRating( + rating: number, + queueType: string, + exclude?: string[] + ): Promise { + let closest: QueueEntry | null = null; + let closestDiff = Infinity; + + for (const entry of this.entries.values()) { + if (entry.queueType !== queueType) continue; + if (exclude?.includes(entry.team.id)) continue; + + const teamRating = getAverageRating(entry.team); + const diff = Math.abs(teamRating - rating); + + if (diff < closestDiff) { + closestDiff = diff; + closest = entry; + } + } + + return closest; + } + + async findAll(): Promise { + return Array.from(this.entries.values()); + } + + async clear(): Promise { + this.entries.clear(); + } +} diff --git a/backend/src/infrastructure/mocks/InMemoryTeamRepository.ts b/backend/src/infrastructure/mocks/InMemoryTeamRepository.ts new file mode 100644 index 0000000..9025482 --- /dev/null +++ b/backend/src/infrastructure/mocks/InMemoryTeamRepository.ts @@ -0,0 +1,36 @@ +import type { ITeamRepository } from "../../domain/interfaces/ITeamRepository"; +import type { Team } from "../../domain/models/Team"; + +export class InMemoryTeamRepository implements ITeamRepository { + private teams: Map = new Map(); + + async findById(id: string): Promise { + return this.teams.get(id) ?? null; + } + + async findByPlayer(playerId: string): Promise { + const result: Team[] = []; + for (const team of this.teams.values()) { + if (team.players.some((p) => p.id === playerId)) { + result.push(team); + } + } + return result; + } + + async save(team: Team): Promise { + this.teams.set(team.id, team); + } + + async delete(teamId: string): Promise { + this.teams.delete(teamId); + } + + async findAll(): Promise { + return Array.from(this.teams.values()); + } + + clear(): void { + this.teams.clear(); + } +} diff --git a/backend/src/infrastructure/mocks/MockGameService.ts b/backend/src/infrastructure/mocks/MockGameService.ts new file mode 100644 index 0000000..37788c0 --- /dev/null +++ b/backend/src/infrastructure/mocks/MockGameService.ts @@ -0,0 +1,136 @@ +import type { IGameService } from "../../domain/interfaces/IGameService"; +import type { Match } from "../../domain/models/Match"; +import type { Player } from "../../domain/models/Player"; +import type { Score } from "../../domain/models/Score"; + +interface MockLobby { + id: string; + matchId: string; + players: Set; + messages: string[]; + currentMap?: number; +} + +export class MockGameService implements IGameService { + public lobbies: Map = new Map(); + public messageLog: Array<{ lobbyId: string; message: string }> = []; + + private playerJoinCallbacks: Array<(osuId: number, lobbyId: string) => void> = []; + private playerLeaveCallbacks: Array<(osuId: number, lobbyId: string) => void> = []; + private matchStartCallbacks: Array<(lobbyId: string) => void> = []; + private matchFinishCallbacks: Array<(lobbyId: string, scores: Score[]) => void> = []; + private rollCallbacks: Array<(osuId: number, lobbyId: string, roll: number) => void> = []; + + async createLobby(match: Match): Promise { + const lobbyId = `mock-lobby-${match.id}`; + this.lobbies.set(lobbyId, { + id: lobbyId, + matchId: match.id, + players: new Set(), + messages: [], + }); + return lobbyId; + } + + async closeLobby(lobbyId: string): Promise { + this.lobbies.delete(lobbyId); + } + + async invitePlayer(player: Player, lobbyId: string): Promise { + const lobby = this.lobbies.get(lobbyId); + if (lobby) { + lobby.messages.push(`Invited ${player.osuName}`); + } + } + + async sendMessage(lobbyId: string, message: string): Promise { + const lobby = this.lobbies.get(lobbyId); + if (lobby) { + lobby.messages.push(message); + } + this.messageLog.push({ lobbyId, message }); + } + + async setMap(lobbyId: string, mapId: number): Promise { + const lobby = this.lobbies.get(lobbyId); + if (lobby) { + lobby.currentMap = mapId; + lobby.messages.push(`Changed map to ${mapId}`); + } + } + + async startMatch(lobbyId: string): Promise { + const lobby = this.lobbies.get(lobbyId); + if (lobby) { + lobby.messages.push("Match started"); + } + } + + onPlayerJoin(callback: (osuId: number, lobbyId: string) => void): void { + this.playerJoinCallbacks.push(callback); + } + + onPlayerLeave(callback: (osuId: number, lobbyId: string) => void): void { + this.playerLeaveCallbacks.push(callback); + } + + onMatchStart(callback: (lobbyId: string) => void): void { + this.matchStartCallbacks.push(callback); + } + + onMatchFinish(callback: (lobbyId: string, scores: Score[]) => void): void { + this.matchFinishCallbacks.push(callback); + } + + onRoll(callback: (osuId: number, lobbyId: string, roll: number) => void): void { + this.rollCallbacks.push(callback); + } + + simulatePlayerJoin(lobbyId: string, osuId: number): void { + const lobby = this.lobbies.get(lobbyId); + if (lobby) { + lobby.players.add(osuId); + } + for (const cb of this.playerJoinCallbacks) { + cb(osuId, lobbyId); + } + } + + simulatePlayerLeave(lobbyId: string, osuId: number): void { + const lobby = this.lobbies.get(lobbyId); + if (lobby) { + lobby.players.delete(osuId); + } + for (const cb of this.playerLeaveCallbacks) { + cb(osuId, lobbyId); + } + } + + simulateRoll(lobbyId: string, osuId: number, roll: number): void { + for (const cb of this.rollCallbacks) { + cb(osuId, lobbyId, roll); + } + } + + simulateMatchStart(lobbyId: string): void { + for (const cb of this.matchStartCallbacks) { + cb(lobbyId); + } + } + + simulateMatchFinish(lobbyId: string, scores: Score[]): void { + for (const cb of this.matchFinishCallbacks) { + cb(lobbyId, scores); + } + } + + clear(): void { + this.lobbies.clear(); + this.messageLog = []; + this.playerJoinCallbacks = []; + this.playerLeaveCallbacks = []; + this.matchStartCallbacks = []; + this.matchFinishCallbacks = []; + this.rollCallbacks = []; + } +} diff --git a/backend/src/infrastructure/mocks/MockNotificationService.ts b/backend/src/infrastructure/mocks/MockNotificationService.ts new file mode 100644 index 0000000..98d1ce4 --- /dev/null +++ b/backend/src/infrastructure/mocks/MockNotificationService.ts @@ -0,0 +1,50 @@ +import type { INotificationService } from "../../domain/interfaces/INotificationService"; +import type { Match } from "../../domain/models/Match"; +import type { Team } from "../../domain/models/Team"; +import type { MatchScore } from "../../domain/models/Score"; + +interface Notification { + type: string; + targetId?: string; + message?: string; + match?: Match; + winner?: Team; + scores?: MatchScore; +} + +export class MockNotificationService implements INotificationService { + public notifications: Notification[] = []; + public announcements: Match[] = []; + public playerNotifications: Array<{ playerId: string; message: string }> = []; + public teamNotifications: Array<{ teamId: string; message: string }> = []; + + async announceMatch(match: Match): Promise { + this.announcements.push(match); + this.notifications.push({ type: "match_announced", match }); + } + + async announceMatchResult(match: Match, winner: Team): Promise { + this.notifications.push({ type: "match_result", match, winner }); + } + + async notifyPlayer(playerId: string, message: string): Promise { + this.playerNotifications.push({ playerId, message }); + this.notifications.push({ type: "player_notification", targetId: playerId, message }); + } + + async notifyTeam(teamId: string, message: string): Promise { + this.teamNotifications.push({ teamId, message }); + this.notifications.push({ type: "team_notification", targetId: teamId, message }); + } + + async announceMapResult(match: Match, scores: MatchScore): Promise { + this.notifications.push({ type: "map_result", match, scores }); + } + + clear(): void { + this.notifications = []; + this.announcements = []; + this.playerNotifications = []; + this.teamNotifications = []; + } +} diff --git a/backend/src/infrastructure/mocks/index.ts b/backend/src/infrastructure/mocks/index.ts new file mode 100644 index 0000000..b036d12 --- /dev/null +++ b/backend/src/infrastructure/mocks/index.ts @@ -0,0 +1,6 @@ +export * from "./InMemoryPlayerRepository"; +export * from "./InMemoryTeamRepository"; +export * from "./InMemoryMatchRepository"; +export * from "./InMemoryQueueRepository"; +export * from "./MockGameService"; +export * from "./MockNotificationService"; diff --git a/backend/src/infrastructure/osu/OsuService.ts b/backend/src/infrastructure/osu/OsuService.ts new file mode 100644 index 0000000..c4d4ec5 --- /dev/null +++ b/backend/src/infrastructure/osu/OsuService.ts @@ -0,0 +1,187 @@ +import type { IGameService } from "../../domain/interfaces/IGameService"; +import type { Match } from "../../domain/models/Match"; +import type { Player } from "../../domain/models/Player"; +import type { Score } from "../../domain/models/Score"; + +type PlayerJoinCallback = (osuId: number, lobbyId: string) => void; +type PlayerLeaveCallback = (osuId: number, lobbyId: string) => void; +type MatchStartCallback = (lobbyId: string) => void; +type MatchFinishCallback = (lobbyId: string, scores: Score[]) => void; +type RollCallback = (osuId: number, lobbyId: string, roll: number) => void; + +export interface OsuApiConfig { + clientId: string; + clientSecret: string; +} + +export interface OsuIrcConfig { + username: string; + password: string; + server?: string; + port?: number; +} + +export class OsuService implements IGameService { + private playerJoinCallbacks: PlayerJoinCallback[] = []; + private playerLeaveCallbacks: PlayerLeaveCallback[] = []; + private matchStartCallbacks: MatchStartCallback[] = []; + private matchFinishCallbacks: MatchFinishCallback[] = []; + private rollCallbacks: RollCallback[] = []; + + private lobbies: Map = new Map(); + + constructor( + private apiConfig: OsuApiConfig, + private ircConfig: OsuIrcConfig + ) { + this.connectIrc(); + } + + private async connectIrc(): Promise { + // TODO: Implement IRC connection using Bun's WebSocket + // Connect to irc.ppy.sh + // Authenticate with BanchoBot + console.log("[OsuService] IRC connection not yet implemented"); + } + + async createLobby(match: Match): Promise { + const lobbyName = `VASH: ${match.teams.map((t) => t.name).join(" vs ")}`; + // TODO: Send IRC command to create lobby + // !mp make + // Parse response to get lobby ID + + const lobbyId = `osu_${match.id.slice(0, 8)}`; + this.lobbies.set(lobbyId, { + matchId: match.id, + channel: `#mp_${lobbyId}`, + }); + + console.log(`[OsuService] Created lobby: ${lobbyId}`); + return lobbyId; + } + + async closeLobby(lobbyId: string): Promise { + // TODO: Send IRC command to close lobby + // !mp close + this.lobbies.delete(lobbyId); + console.log(`[OsuService] Closed lobby: ${lobbyId}`); + } + + async invitePlayer(player: Player, lobbyId: string): Promise { + const lobby = this.lobbies.get(lobbyId); + if (!lobby) { + throw new Error(`Lobby ${lobbyId} not found`); + } + // TODO: Send IRC command + // !mp invite + console.log(`[OsuService] Invited ${player.osuName} to ${lobbyId}`); + } + + async sendMessage(lobbyId: string, message: string): Promise { + const lobby = this.lobbies.get(lobbyId); + if (!lobby) { + throw new Error(`Lobby ${lobbyId} not found`); + } + // TODO: Send IRC message to lobby channel + console.log(`[OsuService] [${lobbyId}] ${message}`); + } + + async setMap(lobbyId: string, mapId: number): Promise { + const lobby = this.lobbies.get(lobbyId); + if (!lobby) { + throw new Error(`Lobby ${lobbyId} not found`); + } + // TODO: Send IRC command + // !mp map + console.log(`[OsuService] Set map ${mapId} in ${lobbyId}`); + } + + async startMatch(lobbyId: string): Promise { + const lobby = this.lobbies.get(lobbyId); + if (!lobby) { + throw new Error(`Lobby ${lobbyId} not found`); + } + // TODO: Send IRC command + // !mp start + console.log(`[OsuService] Started match in ${lobbyId}`); + } + + onPlayerJoin(callback: PlayerJoinCallback): void { + this.playerJoinCallbacks.push(callback); + } + + onPlayerLeave(callback: PlayerLeaveCallback): void { + this.playerLeaveCallbacks.push(callback); + } + + onMatchStart(callback: MatchStartCallback): void { + this.matchStartCallbacks.push(callback); + } + + onMatchFinish(callback: MatchFinishCallback): void { + this.matchFinishCallbacks.push(callback); + } + + onRoll(callback: RollCallback): void { + this.rollCallbacks.push(callback); + } + + // IRC message parsing - called when messages are received + private handleIrcMessage(channel: string, user: string, message: string): void { + // Find lobby by channel + let lobbyId: string | null = null; + for (const [id, lobby] of this.lobbies) { + if (lobby.channel === channel) { + lobbyId = id; + break; + } + } + if (!lobbyId) return; + + // Parse BanchoBot messages + if (user === "BanchoBot") { + // Player joined: " joined in slot X" + const joinMatch = message.match(/^(\S+) joined in slot \d+/); + if (joinMatch) { + // TODO: Look up osuId from username + // For now, we'd need an API call or cache + console.log(`[OsuService] Player joined: ${joinMatch[1]}`); + } + + // Match finished - parse scores + // Format varies, typically shows all player scores + if (message.includes("The match has finished")) { + // TODO: Parse scores from recent messages or API + console.log(`[OsuService] Match finished in ${lobbyId}`); + } + } + + // Parse roll command: " rolls X point(s)" + const rollMatch = message.match(/^(\S+) rolls (\d+) point/); + if (rollMatch) { + const username = rollMatch[1]; + const roll = parseInt(rollMatch[2]); + // TODO: Look up osuId from username + console.log(`[OsuService] ${username} rolled ${roll}`); + } + } + + // Expose for testing + _triggerPlayerJoin(osuId: number, lobbyId: string): void { + for (const cb of this.playerJoinCallbacks) { + cb(osuId, lobbyId); + } + } + + _triggerRoll(osuId: number, lobbyId: string, roll: number): void { + for (const cb of this.rollCallbacks) { + cb(osuId, lobbyId, roll); + } + } + + _triggerMatchFinish(lobbyId: string, scores: Score[]): void { + for (const cb of this.matchFinishCallbacks) { + cb(lobbyId, scores); + } + } +} diff --git a/backend/src/infrastructure/osu/index.ts b/backend/src/infrastructure/osu/index.ts new file mode 100644 index 0000000..c2eeb6c --- /dev/null +++ b/backend/src/infrastructure/osu/index.ts @@ -0,0 +1 @@ +export * from "./OsuService"; diff --git a/backend/src/infrastructure/websocket/WebSocketService.ts b/backend/src/infrastructure/websocket/WebSocketService.ts new file mode 100644 index 0000000..16206d0 --- /dev/null +++ b/backend/src/infrastructure/websocket/WebSocketService.ts @@ -0,0 +1,220 @@ +import type { ServerWebSocket } from "bun"; +import type { EventBus } from "../../application/events/EventBus"; +import type { DomainEvent } from "../../domain/events/DomainEvent"; + +interface WebSocketData { + id: string; + subscribedMatches: Set; +} + +type WebSocketClient = ServerWebSocket; + +export interface ClientMessage { + type: string; + matchId?: string; + payload?: unknown; +} + +export class WebSocketService { + private clients: Map = new Map(); + private matchSubscriptions: Map> = new Map(); + + constructor(private eventBus: EventBus) { + this.setupEventSubscriptions(); + } + + private setupEventSubscriptions(): void { + const eventsToForward = [ + "match.created", + "match.state_changed", + "match.player_joined", + "match.roll_completed", + "match.map_picked", + "match.map_banned", + "match.map_started", + "match.map_completed", + "match.completed", + "match.cancelled", + "queue.team_joined", + "queue.team_left", + "queue.match_found", + ]; + + for (const eventType of eventsToForward) { + this.eventBus.subscribe(eventType as any, (event: DomainEvent) => { + this.broadcastEvent(event); + }); + } + } + + handleOpen(ws: WebSocketClient): void { + const clientId = crypto.randomUUID(); + ws.data = { + id: clientId, + subscribedMatches: new Set(), + }; + this.clients.set(clientId, ws); + console.log(`[WebSocket] Client connected: ${clientId}`); + + ws.send(JSON.stringify({ type: "connected", clientId })); + } + + handleMessage(ws: WebSocketClient, message: string | Buffer): void { + try { + const msg: ClientMessage = + typeof message === "string" ? JSON.parse(message) : JSON.parse(message.toString()); + + switch (msg.type) { + case "subscribe": + if (msg.matchId) { + this.subscribeToMatch(ws, msg.matchId); + } + break; + + case "unsubscribe": + if (msg.matchId) { + this.unsubscribeFromMatch(ws, msg.matchId); + } + break; + + case "ping": + ws.send(JSON.stringify({ type: "pong" })); + break; + + default: + console.log(`[WebSocket] Unknown message type: ${msg.type}`); + } + } catch (error) { + console.error("[WebSocket] Failed to parse message:", error); + } + } + + handleClose(ws: WebSocketClient): void { + const clientId = ws.data?.id; + if (clientId) { + // Clean up subscriptions + for (const matchId of ws.data.subscribedMatches) { + const subs = this.matchSubscriptions.get(matchId); + if (subs) { + subs.delete(clientId); + if (subs.size === 0) { + this.matchSubscriptions.delete(matchId); + } + } + } + + this.clients.delete(clientId); + console.log(`[WebSocket] Client disconnected: ${clientId}`); + } + } + + subscribeToMatch(ws: WebSocketClient, matchId: string): void { + const clientId = ws.data?.id; + if (!clientId) return; + + ws.data.subscribedMatches.add(matchId); + + if (!this.matchSubscriptions.has(matchId)) { + this.matchSubscriptions.set(matchId, new Set()); + } + this.matchSubscriptions.get(matchId)!.add(clientId); + + ws.send(JSON.stringify({ type: "subscribed", matchId })); + console.log(`[WebSocket] Client ${clientId} subscribed to match ${matchId}`); + } + + unsubscribeFromMatch(ws: WebSocketClient, matchId: string): void { + const clientId = ws.data?.id; + if (!clientId) return; + + ws.data.subscribedMatches.delete(matchId); + + const subs = this.matchSubscriptions.get(matchId); + if (subs) { + subs.delete(clientId); + if (subs.size === 0) { + this.matchSubscriptions.delete(matchId); + } + } + + ws.send(JSON.stringify({ type: "unsubscribed", matchId })); + } + + private broadcastEvent(event: DomainEvent): void { + const payload = event.payload as any; + const matchId = payload?.match?.id; + + const message = JSON.stringify({ + type: "event", + eventType: event.type, + timestamp: event.timestamp.toISOString(), + payload: this.serializePayload(payload), + }); + + if (matchId) { + // Send to match subscribers + const subscribers = this.matchSubscriptions.get(matchId); + if (subscribers) { + for (const clientId of subscribers) { + const client = this.clients.get(clientId); + if (client) { + client.send(message); + } + } + } + } else { + // Broadcast to all clients + for (const client of this.clients.values()) { + client.send(message); + } + } + } + + private serializePayload(payload: any): any { + if (!payload) return payload; + + // Handle Maps + if (payload instanceof Map) { + return Object.fromEntries(payload); + } + + // Handle Sets + if (payload instanceof Set) { + return Array.from(payload); + } + + // Handle objects with Maps/Sets + if (typeof payload === "object") { + const result: any = {}; + for (const [key, value] of Object.entries(payload)) { + if (value instanceof Map) { + result[key] = Object.fromEntries(value); + } else if (value instanceof Set) { + result[key] = Array.from(value); + } else if (typeof value === "object" && value !== null) { + result[key] = this.serializePayload(value); + } else { + result[key] = value; + } + } + return result; + } + + return payload; + } + + broadcast(message: object): void { + const msg = JSON.stringify(message); + for (const client of this.clients.values()) { + client.send(msg); + } + } + + getConnectedCount(): number { + return this.clients.size; + } + + getMatchSubscriberCount(matchId: string): number { + return this.matchSubscriptions.get(matchId)?.size ?? 0; + } +} diff --git a/backend/src/infrastructure/websocket/index.ts b/backend/src/infrastructure/websocket/index.ts new file mode 100644 index 0000000..6be4791 --- /dev/null +++ b/backend/src/infrastructure/websocket/index.ts @@ -0,0 +1 @@ +export * from "./WebSocketService"; diff --git a/backend/src/presentation/api/routes.ts b/backend/src/presentation/api/routes.ts new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/testing/TestHarness.ts b/backend/src/testing/TestHarness.ts new file mode 100644 index 0000000..316bf6d --- /dev/null +++ b/backend/src/testing/TestHarness.ts @@ -0,0 +1,217 @@ +import { EventBus } from "../application/events/EventBus"; +import { MatchService } from "../application/services/MatchService"; +import { QueueService } from "../application/services/QueueService"; +import { ProtocolRegistry } from "../domain/protocols/ProtocolRegistry"; +import { StandardOsuProtocol } from "../domain/protocols/StandardOsuProtocol"; +import { HeadToHeadProtocol } from "../domain/protocols/HeadToHeadProtocol"; +import { InMemoryPlayerRepository } from "../infrastructure/mocks/InMemoryPlayerRepository"; +import { InMemoryTeamRepository } from "../infrastructure/mocks/InMemoryTeamRepository"; +import { InMemoryMatchRepository } from "../infrastructure/mocks/InMemoryMatchRepository"; +import { InMemoryQueueRepository } from "../infrastructure/mocks/InMemoryQueueRepository"; +import { MockGameService } from "../infrastructure/mocks/MockGameService"; +import { MockNotificationService } from "../infrastructure/mocks/MockNotificationService"; +import { type Player, createPlayer } from "../domain/models/Player"; +import { type Team, createTeam } from "../domain/models/Team"; +import type { Match } from "../domain/models/Match"; +import type { Score } from "../domain/models/Score"; +import { MatchState } from "../domain/models/MatchState"; +import type { DomainEvent } from "../domain/events/DomainEvent"; + +export class TestHarness { + public eventBus: EventBus; + public mockGameService: MockGameService; + public mockNotificationService: MockNotificationService; + public matchService: MatchService; + public queueService: QueueService; + public protocolRegistry: ProtocolRegistry; + + public playerRepo: InMemoryPlayerRepository; + public teamRepo: InMemoryTeamRepository; + public matchRepo: InMemoryMatchRepository; + public queueRepo: InMemoryQueueRepository; + + public events: DomainEvent[] = []; + + private playerCounter = 1; + private teamCounter = 1; + + constructor() { + this.eventBus = new EventBus(); + this.mockGameService = new MockGameService(); + this.mockNotificationService = new MockNotificationService(); + + this.playerRepo = new InMemoryPlayerRepository(); + this.teamRepo = new InMemoryTeamRepository(); + this.matchRepo = new InMemoryMatchRepository(); + this.queueRepo = new InMemoryQueueRepository(); + + this.protocolRegistry = new ProtocolRegistry(); + this.protocolRegistry.register(new StandardOsuProtocol()); + this.protocolRegistry.register(new HeadToHeadProtocol()); + + this.matchService = new MatchService( + this.matchRepo, + this.playerRepo, + this.protocolRegistry, + this.mockGameService, + this.mockNotificationService, + this.eventBus + ); + + this.queueService = new QueueService( + this.queueRepo, + this.matchService, + this.eventBus, + { minQueueTime: 0, protocol: "head-to-head" } + ); + + this.eventBus.subscribeAll((event) => { + this.events.push(event); + }); + } + + createPlayer(overrides?: Partial): Player { + const id = this.playerCounter++; + const player = createPlayer({ + discordId: `discord-${id}`, + osuId: 100000 + id, + osuName: `Player${id}`, + displayName: `Player ${id}`, + rating: 1000, + ...overrides, + }); + + this.playerRepo.save(player); + return player; + } + + createPlayers(count: number, baseRating: number = 1000): Player[] { + const players: Player[] = []; + for (let i = 0; i < count; i++) { + players.push(this.createPlayer({ rating: baseRating })); + } + return players; + } + + createTeam(players: Player[], overrides?: Partial): Team { + const id = this.teamCounter++; + const team = createTeam({ + name: overrides?.name ?? `Team ${id}`, + players, + ...overrides, + }); + + this.teamRepo.save(team); + return team; + } + + async simulateAllPlayersJoin(match: Match): Promise { + for (const team of match.teams) { + for (const player of team.players) { + if (match.lobbyId) { + this.mockGameService.simulatePlayerJoin(match.lobbyId, player.osuId); + } + await this.matchService.handlePlayerJoined(match.id, player); + } + } + } + + async simulateRolls( + match: Match, + rolls: Map + ): Promise { + for (const team of match.teams) { + const roll = rolls.get(team.id) ?? Math.floor(Math.random() * 100) + 1; + const captain = team.captain; + if (match.lobbyId) { + this.mockGameService.simulateRoll(match.lobbyId, captain.osuId, roll); + } + await this.matchService.handleRoll(match.id, captain, roll); + } + } + + async simulateMapComplete(match: Match, winningTeamId: string): Promise { + const scores: Score[] = []; + const winningTeam = match.teams.find((t) => t.id === winningTeamId)!; + const losingTeam = match.teams.find((t) => t.id !== winningTeamId)!; + + for (const player of winningTeam.players) { + scores.push({ + playerId: player.id, + score: 500000 + Math.floor(Math.random() * 100000), + accuracy: 95 + Math.random() * 5, + maxCombo: 800 + Math.floor(Math.random() * 200), + misses: Math.floor(Math.random() * 5), + }); + } + + for (const player of losingTeam.players) { + scores.push({ + playerId: player.id, + score: 400000 + Math.floor(Math.random() * 80000), + accuracy: 90 + Math.random() * 8, + maxCombo: 600 + Math.floor(Math.random() * 200), + misses: Math.floor(Math.random() * 10), + }); + } + + if (match.lobbyId) { + this.mockGameService.simulateMatchFinish(match.lobbyId, scores); + } + await this.matchService.handleMapComplete(match.id, scores); + } + + async runFullMatch( + team1: Team, + team2: Team, + protocolName: string = "head-to-head" + ): Promise { + await this.queueService.joinQueue(team1, "ranked"); + await this.queueService.joinQueue(team2, "ranked"); + + const match = await this.queueService.findMatch("ranked"); + if (!match) { + throw new Error("Failed to create match"); + } + + await this.simulateAllPlayersJoin(match); + + const rolls = new Map(); + rolls.set(team1.id, 75); + rolls.set(team2.id, 50); + await this.simulateRolls(match, rolls); + + const protocol = this.protocolRegistry.get(protocolName); + const winsNeeded = protocol.getWinsNeeded(); + + for (let i = 0; i < winsNeeded; i++) { + const updatedMatch = await this.matchRepo.findById(match.id); + if (!updatedMatch || updatedMatch.state === MatchState.COMPLETED) break; + + await this.simulateMapComplete(match, team1.id); + } + + return (await this.matchRepo.findById(match.id))!; + } + + getEventsByType(type: string): DomainEvent[] { + return this.events.filter((e) => e.type === type); + } + + clearEvents(): void { + this.events = []; + } + + reset(): void { + this.playerRepo.clear(); + this.teamRepo.clear(); + this.matchRepo.clear(); + this.queueRepo.clear(); + this.mockGameService.clear(); + this.mockNotificationService.clear(); + this.eventBus.clear(); + this.events = []; + this.playerCounter = 1; + this.teamCounter = 1; + } +} diff --git a/backend/src/testing/index.ts b/backend/src/testing/index.ts new file mode 100644 index 0000000..202c7a8 --- /dev/null +++ b/backend/src/testing/index.ts @@ -0,0 +1 @@ +export * from "./TestHarness"; diff --git a/backend/tests/application/QueueService.test.ts b/backend/tests/application/QueueService.test.ts new file mode 100644 index 0000000..4a1f27f --- /dev/null +++ b/backend/tests/application/QueueService.test.ts @@ -0,0 +1,97 @@ +import { test, expect, beforeEach } from "bun:test"; +import { TestHarness } from "../../src/testing/TestHarness"; + +let harness: TestHarness; + +beforeEach(() => { + harness = new TestHarness(); +}); + +test("team can join queue", async () => { + const players = harness.createPlayers(1); + const team = harness.createTeam(players, { name: "Test Team" }); + + const entry = await harness.queueService.joinQueue(team, "ranked"); + + expect(entry.team.id).toBe(team.id); + expect(entry.queueType).toBe("ranked"); +}); + +test("team cannot join queue twice", async () => { + const players = harness.createPlayers(1); + const team = harness.createTeam(players, { name: "Test Team" }); + + await harness.queueService.joinQueue(team, "ranked"); + + expect(async () => { + await harness.queueService.joinQueue(team, "ranked"); + }).toThrow(); +}); + +test("team can leave queue", async () => { + const players = harness.createPlayers(1); + const team = harness.createTeam(players, { name: "Test Team" }); + + await harness.queueService.joinQueue(team, "ranked"); + await harness.queueService.leaveQueue(team.id); + + const status = await harness.queueService.getQueueStatus("ranked"); + expect(status.count).toBe(0); +}); + +test("findMatch returns null when less than 2 teams in queue", async () => { + const players = harness.createPlayers(1); + const team = harness.createTeam(players, { name: "Test Team" }); + + await harness.queueService.joinQueue(team, "ranked"); + + const match = await harness.queueService.findMatch("ranked"); + expect(match).toBeNull(); +}); + +test("findMatch creates match when 2 teams in queue", async () => { + const players1 = harness.createPlayers(1, 1000); + const players2 = harness.createPlayers(1, 1010); + + const team1 = harness.createTeam(players1, { name: "Team 1" }); + const team2 = harness.createTeam(players2, { name: "Team 2" }); + + await harness.queueService.joinQueue(team1, "ranked"); + await harness.queueService.joinQueue(team2, "ranked"); + + const match = await harness.queueService.findMatch("ranked"); + + expect(match).not.toBeNull(); + expect(match!.teams.length).toBe(2); +}); + +test("getQueueStatus returns correct count", async () => { + const players1 = harness.createPlayers(1); + const players2 = harness.createPlayers(1); + + const team1 = harness.createTeam(players1, { name: "Team 1" }); + const team2 = harness.createTeam(players2, { name: "Team 2" }); + + await harness.queueService.joinQueue(team1, "ranked"); + await harness.queueService.joinQueue(team2, "ranked"); + + const status = await harness.queueService.getQueueStatus("ranked"); + + expect(status.count).toBe(2); + expect(status.entries.length).toBe(2); +}); + +test("queue events are published", async () => { + const players = harness.createPlayers(1); + const team = harness.createTeam(players, { name: "Test Team" }); + + await harness.queueService.joinQueue(team, "ranked"); + + const joinEvents = harness.getEventsByType("queue.team_joined"); + expect(joinEvents.length).toBe(1); + + await harness.queueService.leaveQueue(team.id); + + const leaveEvents = harness.getEventsByType("queue.team_left"); + expect(leaveEvents.length).toBe(1); +}); diff --git a/backend/tests/domain/protocols/StandardOsuProtocol.test.ts b/backend/tests/domain/protocols/StandardOsuProtocol.test.ts new file mode 100644 index 0000000..64a883b --- /dev/null +++ b/backend/tests/domain/protocols/StandardOsuProtocol.test.ts @@ -0,0 +1,155 @@ +import { test, expect, beforeEach } from "bun:test"; +import { StandardOsuProtocol } from "../../../src/domain/protocols/StandardOsuProtocol"; +import { createMatch } from "../../../src/domain/models/Match"; +import { createTeam } from "../../../src/domain/models/Team"; +import { createPlayer } from "../../../src/domain/models/Player"; +import type { Score } from "../../../src/domain/models/Score"; + +let protocol: StandardOsuProtocol; + +beforeEach(() => { + protocol = new StandardOsuProtocol(); +}); + +test("has correct configuration", () => { + expect(protocol.name).toBe("standard-osu"); + expect(protocol.teamSize).toBe(4); + expect(protocol.bestOf).toBe(9); + expect(protocol.hasPickBan).toBe(true); + expect(protocol.hasFreemod).toBe(true); +}); + +test("getWinsNeeded returns correct value for best of 9", () => { + expect(protocol.getWinsNeeded()).toBe(5); +}); + +test("calculateMapWinner returns winning team based on scores", () => { + const players1 = [ + createPlayer({ id: "p1", discordId: "d1", osuId: 1, osuName: "Player1" }), + createPlayer({ id: "p2", discordId: "d2", osuId: 2, osuName: "Player2" }), + ]; + const players2 = [ + createPlayer({ id: "p3", discordId: "d3", osuId: 3, osuName: "Player3" }), + createPlayer({ id: "p4", discordId: "d4", osuId: 4, osuName: "Player4" }), + ]; + + const team1 = createTeam({ id: "team1", name: "Team 1", players: players1 }); + const team2 = createTeam({ id: "team2", name: "Team 2", players: players2 }); + + const match = createMatch({ + teams: [team1, team2], + protocol: "standard-osu", + }); + + const scores: Score[] = [ + { playerId: "p1", score: 500000, accuracy: 95, maxCombo: 800, misses: 2 }, + { playerId: "p2", score: 450000, accuracy: 94, maxCombo: 750, misses: 3 }, + { playerId: "p3", score: 400000, accuracy: 92, maxCombo: 700, misses: 5 }, + { playerId: "p4", score: 380000, accuracy: 91, maxCombo: 680, misses: 6 }, + ]; + + const winner = protocol.calculateMapWinner(match, scores); + expect(winner).toBe("team1"); +}); + +test("calculateMatchWinner returns null when no team has enough wins", () => { + const players1 = [createPlayer({ id: "p1", discordId: "d1", osuId: 1, osuName: "P1" })]; + const players2 = [createPlayer({ id: "p2", discordId: "d2", osuId: 2, osuName: "P2" })]; + + const team1 = createTeam({ id: "team1", name: "Team 1", players: players1 }); + const team2 = createTeam({ id: "team2", name: "Team 2", players: players2 }); + + const match = createMatch({ + teams: [team1, team2], + protocol: "standard-osu", + }); + + expect(protocol.calculateMatchWinner(match)).toBeNull(); +}); + +test("isValidPick returns false when not the picking team", () => { + const players1 = [createPlayer({ id: "p1", discordId: "d1", osuId: 1, osuName: "P1" })]; + const players2 = [createPlayer({ id: "p2", discordId: "d2", osuId: 2, osuName: "P2" })]; + + const team1 = createTeam({ id: "team1", name: "Team 1", players: players1 }); + const team2 = createTeam({ id: "team2", name: "Team 2", players: players2 }); + + const match = createMatch({ + teams: [team1, team2], + protocol: "standard-osu", + }); + + match.currentPicker = "team1"; + + const map = { + id: 123, + beatmapSetId: 456, + title: "Test Map", + artist: "Artist", + difficulty: "Hard", + starRating: 5.5, + bpm: 180, + length: 120, + }; + + expect(protocol.isValidPick(match, team1, map)).toBe(true); + expect(protocol.isValidPick(match, team2, map)).toBe(false); +}); + +test("isValidPick returns false for banned maps", () => { + const players1 = [createPlayer({ id: "p1", discordId: "d1", osuId: 1, osuName: "P1" })]; + const players2 = [createPlayer({ id: "p2", discordId: "d2", osuId: 2, osuName: "P2" })]; + + const team1 = createTeam({ id: "team1", name: "Team 1", players: players1 }); + const team2 = createTeam({ id: "team2", name: "Team 2", players: players2 }); + + const match = createMatch({ + teams: [team1, team2], + protocol: "standard-osu", + }); + + match.currentPicker = "team1"; + match.bans.set("team2", [123]); + + const map = { + id: 123, + beatmapSetId: 456, + title: "Test Map", + artist: "Artist", + difficulty: "Hard", + starRating: 5.5, + bpm: 180, + length: 120, + }; + + expect(protocol.isValidPick(match, team1, map)).toBe(false); +}); + +test("isValidPick returns false for already picked maps", () => { + const players1 = [createPlayer({ id: "p1", discordId: "d1", osuId: 1, osuName: "P1" })]; + const players2 = [createPlayer({ id: "p2", discordId: "d2", osuId: 2, osuName: "P2" })]; + + const team1 = createTeam({ id: "team1", name: "Team 1", players: players1 }); + const team2 = createTeam({ id: "team2", name: "Team 2", players: players2 }); + + const match = createMatch({ + teams: [team1, team2], + protocol: "standard-osu", + }); + + match.currentPicker = "team1"; + match.picks.push(123); + + const map = { + id: 123, + beatmapSetId: 456, + title: "Test Map", + artist: "Artist", + difficulty: "Hard", + starRating: 5.5, + bpm: 180, + length: 120, + }; + + expect(protocol.isValidPick(match, team1, map)).toBe(false); +}); diff --git a/backend/tests/integration/full-match-flow.test.ts b/backend/tests/integration/full-match-flow.test.ts new file mode 100644 index 0000000..2c145f9 --- /dev/null +++ b/backend/tests/integration/full-match-flow.test.ts @@ -0,0 +1,121 @@ +import { test, expect, beforeEach } from "bun:test"; +import { TestHarness } from "../../src/testing/TestHarness"; +import { MatchState } from "../../src/domain/models/MatchState"; + +let harness: TestHarness; + +beforeEach(() => { + harness = new TestHarness(); +}); + +test("complete match flow from queue to completion", async () => { + const players1 = harness.createPlayers(1, 1000); + const players2 = harness.createPlayers(1, 1020); + + const team1 = harness.createTeam(players1, { name: "Team Alpha" }); + const team2 = harness.createTeam(players2, { name: "Team Beta" }); + + await harness.queueService.joinQueue(team1, "ranked"); + await harness.queueService.joinQueue(team2, "ranked"); + + const match = await harness.queueService.findMatch("ranked"); + + expect(match).not.toBeNull(); + expect(match!.teams.length).toBe(2); + expect(match!.lobbyId).toBeDefined(); + + await harness.simulateAllPlayersJoin(match!); + + let updatedMatch = await harness.matchRepo.findById(match!.id); + expect(updatedMatch!.state).toBe(MatchState.ROLLING); + + const rolls = new Map(); + rolls.set(team1.id, 75); + rolls.set(team2.id, 50); + await harness.simulateRolls(match!, rolls); + + updatedMatch = await harness.matchRepo.findById(match!.id); + expect(updatedMatch!.state).toBe(MatchState.PICKING); + + const events = harness.getEventsByType("match.created"); + expect(events.length).toBe(1); + + const stateChanges = harness.getEventsByType("match.state_changed"); + expect(stateChanges.length).toBeGreaterThan(0); +}); + +test("match creates lobby on start", async () => { + const players1 = harness.createPlayers(1); + const players2 = harness.createPlayers(1); + + const team1 = harness.createTeam(players1, { name: "Team 1" }); + const team2 = harness.createTeam(players2, { name: "Team 2" }); + + await harness.queueService.joinQueue(team1, "ranked"); + await harness.queueService.joinQueue(team2, "ranked"); + + const match = await harness.queueService.findMatch("ranked"); + + expect(match!.lobbyId).toBeDefined(); + expect(harness.mockGameService.lobbies.has(match!.lobbyId!)).toBe(true); +}); + +test("notifications are sent when match is created", async () => { + const players1 = harness.createPlayers(1); + const players2 = harness.createPlayers(1); + + const team1 = harness.createTeam(players1, { name: "Team 1" }); + const team2 = harness.createTeam(players2, { name: "Team 2" }); + + await harness.queueService.joinQueue(team1, "ranked"); + await harness.queueService.joinQueue(team2, "ranked"); + + await harness.queueService.findMatch("ranked"); + + expect(harness.mockNotificationService.announcements.length).toBe(1); +}); + +test("match state transitions correctly through rolling phase", async () => { + const players1 = harness.createPlayers(1); + const players2 = harness.createPlayers(1); + + const team1 = harness.createTeam(players1, { name: "Team 1" }); + const team2 = harness.createTeam(players2, { name: "Team 2" }); + + await harness.queueService.joinQueue(team1, "ranked"); + await harness.queueService.joinQueue(team2, "ranked"); + + const match = await harness.queueService.findMatch("ranked"); + expect(match!.state).toBe(MatchState.WAITING_PLAYERS); + + await harness.simulateAllPlayersJoin(match!); + + let updatedMatch = await harness.matchRepo.findById(match!.id); + expect(updatedMatch!.state).toBe(MatchState.ROLLING); + + const rolls = new Map(); + rolls.set(team1.id, 75); + rolls.set(team2.id, 50); + await harness.simulateRolls(match!, rolls); + + updatedMatch = await harness.matchRepo.findById(match!.id); + expect(updatedMatch!.state).toBe(MatchState.PICKING); + // In head-to-head protocol, roll winner picks first + expect(updatedMatch!.currentPicker).toBe(team1.id); +}); + +test("queue status is empty after match found", async () => { + const players1 = harness.createPlayers(1); + const players2 = harness.createPlayers(1); + + const team1 = harness.createTeam(players1, { name: "Team 1" }); + const team2 = harness.createTeam(players2, { name: "Team 2" }); + + await harness.queueService.joinQueue(team1, "ranked"); + await harness.queueService.joinQueue(team2, "ranked"); + + await harness.queueService.findMatch("ranked"); + + const status = await harness.queueService.getQueueStatus("ranked"); + expect(status.count).toBe(0); +}); diff --git a/tsconfig.json b/backend/tsconfig.json similarity index 100% rename from tsconfig.json rename to backend/tsconfig.json diff --git a/bun.lock b/bun.lock deleted file mode 100644 index 8ebdea4..0000000 --- a/bun.lock +++ /dev/null @@ -1,26 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "esports", - "devDependencies": { - "@types/bun": "latest", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, - }, - "packages": { - "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], - - "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], - - "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], - - "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=="], - } -} diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..ec707ff --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +# Replace with your DB credentials! +DATABASE_URL="postgres://user:password@host:port/db-name" diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..bff793d --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +test-results +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..0243576 --- /dev/null +++ b/frontend/.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/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..819fa57 --- /dev/null +++ b/frontend/.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/frontend/.vscode/settings.json b/frontend/.vscode/settings.json new file mode 100644 index 0000000..bc31e15 --- /dev/null +++ b/frontend/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "*.css": "tailwindcss" + } +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..10af74d --- /dev/null +++ b/frontend/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,forms" sveltekit-adapter="adapter:node" devtools-json drizzle="database:postgresql+postgresql:postgres.js+docker:no" lucia="demo:yes" --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/frontend/bun.lock b/frontend/bun.lock new file mode 100644 index 0000000..11cd1e6 --- /dev/null +++ b/frontend/bun.lock @@ -0,0 +1,854 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "frontend", + "dependencies": { + "@node-rs/argon2": "^2.0.2", + "postgres": "^3.4.7", + }, + "devDependencies": { + "@eslint/compat": "^1.4.0", + "@eslint/js": "^9.39.1", + "@oslojs/crypto": "^1.0.1", + "@oslojs/encoding": "^1.1.0", + "@playwright/test": "^1.57.0", + "@sveltejs/adapter-node": "^5.4.0", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.1.17", + "@types/node": "^22", + "@vitest/browser-playwright": "^4.0.15", + "drizzle-kit": "^0.31.8", + "drizzle-orm": "^0.45.0", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.13.1", + "globals": "^16.5.0", + "playwright": "^1.57.0", + "prettier": "^3.7.4", + "prettier-plugin-svelte": "^3.4.0", + "prettier-plugin-tailwindcss": "^0.7.2", + "svelte": "^5.45.6", + "svelte-check": "^4.3.4", + "tailwindcss": "^4.1.17", + "typescript": "^5.9.3", + "typescript-eslint": "^8.48.1", + "vite": "^7.2.6", + "vite-plugin-devtools-json": "^1.0.0", + "vitest": "^4.0.15", + "vitest-browser-svelte": "^2.0.1", + }, + }, + }, + "packages": { + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + + "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@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@1.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0" }, "peerDependencies": { "eslint": "^8.40 || 9" }, "optionalPeers": ["eslint"] }, "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w=="], + + "@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@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "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.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], + + "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], + + "@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=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + + "@node-rs/argon2": ["@node-rs/argon2@2.0.2", "", { "optionalDependencies": { "@node-rs/argon2-android-arm-eabi": "2.0.2", "@node-rs/argon2-android-arm64": "2.0.2", "@node-rs/argon2-darwin-arm64": "2.0.2", "@node-rs/argon2-darwin-x64": "2.0.2", "@node-rs/argon2-freebsd-x64": "2.0.2", "@node-rs/argon2-linux-arm-gnueabihf": "2.0.2", "@node-rs/argon2-linux-arm64-gnu": "2.0.2", "@node-rs/argon2-linux-arm64-musl": "2.0.2", "@node-rs/argon2-linux-x64-gnu": "2.0.2", "@node-rs/argon2-linux-x64-musl": "2.0.2", "@node-rs/argon2-wasm32-wasi": "2.0.2", "@node-rs/argon2-win32-arm64-msvc": "2.0.2", "@node-rs/argon2-win32-ia32-msvc": "2.0.2", "@node-rs/argon2-win32-x64-msvc": "2.0.2" } }, "sha512-t64wIsPEtNd4aUPuTAyeL2ubxATCBGmeluaKXEMAFk/8w6AJIVVkeLKMBpgLW6LU2t5cQxT+env/c6jxbtTQBg=="], + + "@node-rs/argon2-android-arm-eabi": ["@node-rs/argon2-android-arm-eabi@2.0.2", "", { "os": "android", "cpu": "arm" }, "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw=="], + + "@node-rs/argon2-android-arm64": ["@node-rs/argon2-android-arm64@2.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg=="], + + "@node-rs/argon2-darwin-arm64": ["@node-rs/argon2-darwin-arm64@2.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3TTNL/7wbcpNju5YcqUrCgXnXUSbD7ogeAKatzBVHsbpjZQbNb1NDxDjqqrWoTt6XL3z9mJUMGwbAk7zQltHtA=="], + + "@node-rs/argon2-darwin-x64": ["@node-rs/argon2-darwin-x64@2.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw=="], + + "@node-rs/argon2-freebsd-x64": ["@node-rs/argon2-freebsd-x64@2.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg=="], + + "@node-rs/argon2-linux-arm-gnueabihf": ["@node-rs/argon2-linux-arm-gnueabihf@2.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww=="], + + "@node-rs/argon2-linux-arm64-gnu": ["@node-rs/argon2-linux-arm64-gnu@2.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew=="], + + "@node-rs/argon2-linux-arm64-musl": ["@node-rs/argon2-linux-arm64-musl@2.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA=="], + + "@node-rs/argon2-linux-x64-gnu": ["@node-rs/argon2-linux-x64-gnu@2.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ZM3jrHuJ0dKOhvA80gKJqBpBRmTJTFSo2+xVZR+phQcbAKRlDMSZMFDiKbSTnctkfwNFtjgDdh5g1vaEV04AvA=="], + + "@node-rs/argon2-linux-x64-musl": ["@node-rs/argon2-linux-x64-musl@2.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw=="], + + "@node-rs/argon2-wasm32-wasi": ["@node-rs/argon2-wasm32-wasi@2.0.2", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.5" }, "cpu": "none" }, "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ=="], + + "@node-rs/argon2-win32-arm64-msvc": ["@node-rs/argon2-win32-arm64-msvc@2.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ=="], + + "@node-rs/argon2-win32-ia32-msvc": ["@node-rs/argon2-win32-ia32-msvc@2.0.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ=="], + + "@node-rs/argon2-win32-x64-msvc": ["@node-rs/argon2-win32-x64-msvc@2.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw=="], + + "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], + + "@oslojs/binary": ["@oslojs/binary@1.0.0", "", {}, "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ=="], + + "@oslojs/crypto": ["@oslojs/crypto@1.0.1", "", { "dependencies": { "@oslojs/asn1": "1.0.0", "@oslojs/binary": "1.0.0" } }, "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ=="], + + "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], + + "@playwright/test": ["@playwright/test@1.58.0", "", { "dependencies": { "playwright": "1.58.0" }, "bin": { "playwright": "cli.js" } }, "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.9", "", { "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-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA=="], + + "@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.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.56.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.56.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="], + + "@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.5.2", "", { "dependencies": { "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "rollup": "^4.9.5" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-L15Djwpr7HrSAPj/Z8PYfc0pa9A1tllrr18phKI0WJHJeoWw45yinPf0IGgVTmakqx1B3JQ+C/OFl9ZwmxHU1Q=="], + + "@sveltejs/kit": ["@sveltejs/kit@2.50.1", "", { "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.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.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", "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" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-XRHD2i3zC4ukhz2iCQzO4mbsts081PAZnnMAQ7LNpWeYgeBmwMsalf0FGSwhFXBbtr2XViPKnFJBDCckWqrsLw=="], + + "@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/forms": ["@tailwindcss/forms@0.5.11", "", { "dependencies": { "mini-svg-data-uri": "^1.2.3" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" } }, "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="], + + "@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.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], + + "@testing-library/svelte-core": ["@testing-library/svelte-core@1.0.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0" } }, "sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@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.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], + + "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/type-utils": "8.53.1", "@typescript-eslint/utils": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.53.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.1", "@typescript-eslint/types": "^8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1" } }, "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.53.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/utils": "8.53.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.53.1", "", {}, "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.53.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.53.1", "@typescript-eslint/tsconfig-utils": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg=="], + + "@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.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.12.6", "", { "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-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "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.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "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=="], + + "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=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "devalue": ["devalue@5.6.2", "", {}, "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg=="], + + "drizzle-kit": ["drizzle-kit@0.31.8", "", { "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-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg=="], + + "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.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], + + "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.2", "", { "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.2", "@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-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], + + "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.14.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", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-Isw0GvaMm0yHxAj71edAdGFh28ufYs+6rk2KlbbZphnqZAzrH3Se3t12IFh2H9+1F/jlDhBBL4oiOJmLqmYX0g=="], + + "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.2", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ=="], + + "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.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], + + "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=="], + + "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=="], + + "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.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + + "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + + "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=="], + + "mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": { "mini-svg-data-uri": "cli.js" } }, "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "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=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "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.0", "", { "dependencies": { "playwright-core": "1.58.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ=="], + + "playwright-core": ["playwright-core@1.58.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw=="], + + "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.4.1", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg=="], + + "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=="], + + "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.56.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.56.0", "@rollup/rollup-android-arm64": "4.56.0", "@rollup/rollup-darwin-arm64": "4.56.0", "@rollup/rollup-darwin-x64": "4.56.0", "@rollup/rollup-freebsd-arm64": "4.56.0", "@rollup/rollup-freebsd-x64": "4.56.0", "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", "@rollup/rollup-linux-arm-musleabihf": "4.56.0", "@rollup/rollup-linux-arm64-gnu": "4.56.0", "@rollup/rollup-linux-arm64-musl": "4.56.0", "@rollup/rollup-linux-loong64-gnu": "4.56.0", "@rollup/rollup-linux-loong64-musl": "4.56.0", "@rollup/rollup-linux-ppc64-gnu": "4.56.0", "@rollup/rollup-linux-ppc64-musl": "4.56.0", "@rollup/rollup-linux-riscv64-gnu": "4.56.0", "@rollup/rollup-linux-riscv64-musl": "4.56.0", "@rollup/rollup-linux-s390x-gnu": "4.56.0", "@rollup/rollup-linux-x64-gnu": "4.56.0", "@rollup/rollup-linux-x64-musl": "4.56.0", "@rollup/rollup-openbsd-x64": "4.56.0", "@rollup/rollup-openharmony-arm64": "4.56.0", "@rollup/rollup-win32-arm64-msvc": "4.56.0", "@rollup/rollup-win32-ia32-msvc": "4.56.0", "@rollup/rollup-win32-x64-gnu": "4.56.0", "@rollup/rollup-win32-x64-msvc": "4.56.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg=="], + + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + + "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.48.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-+NUe82VoFP1RQViZI/esojx70eazGF4u0O/9ucqZ4rPcOZD+n5EVp17uYsqwdzjUjZyTpGKunHbDziW6AIAVkQ=="], + + "svelte-check": ["svelte-check@4.3.5", "", { "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-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q=="], + + "svelte-eslint-parser": ["svelte-eslint-parser@1.4.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" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA=="], + + "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], + + "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=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "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=="], + + "typescript-eslint": ["typescript-eslint@8.53.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.1", "@typescript-eslint/parser": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/utils": "8.53.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg=="], + + "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.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], + + "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=="], + + "@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/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@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@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "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.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + + "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@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + } +} diff --git a/frontend/drizzle.config.ts b/frontend/drizzle.config.ts new file mode 100644 index 0000000..d57e081 --- /dev/null +++ b/frontend/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/frontend/e2e/demo.test.ts b/frontend/e2e/demo.test.ts new file mode 100644 index 0000000..9985ce1 --- /dev/null +++ b/frontend/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/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..7df665d --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,41 @@ +import prettier from 'eslint-config-prettier'; +import { fileURLToPath } from 'node:url'; +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 = fileURLToPath(new URL('./.gitignore', import.meta.url)); + +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/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..79cdc92 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,61 @@ +{ + "name": "frontend", + "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" + }, + "devDependencies": { + "@eslint/compat": "^1.4.0", + "@eslint/js": "^9.39.1", + "@oslojs/crypto": "^1.0.1", + "@oslojs/encoding": "^1.1.0", + "@playwright/test": "^1.57.0", + "@sveltejs/adapter-node": "^5.4.0", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.1.17", + "@types/node": "^22", + "@vitest/browser-playwright": "^4.0.15", + "drizzle-kit": "^0.31.8", + "drizzle-orm": "^0.45.0", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.13.1", + "globals": "^16.5.0", + "playwright": "^1.57.0", + "prettier": "^3.7.4", + "prettier-plugin-svelte": "^3.4.0", + "prettier-plugin-tailwindcss": "^0.7.2", + "svelte": "^5.45.6", + "svelte-check": "^4.3.4", + "tailwindcss": "^4.1.17", + "typescript": "^5.9.3", + "typescript-eslint": "^8.48.1", + "vite": "^7.2.6", + "vite-plugin-devtools-json": "^1.0.0", + "vitest": "^4.0.15", + "vitest-browser-svelte": "^2.0.1" + }, + "dependencies": { + "@node-rs/argon2": "^2.0.2", + "postgres": "^3.4.7" + } +} diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..8f5062c --- /dev/null +++ b/frontend/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/frontend/src/app.d.ts b/frontend/src/app.d.ts new file mode 100644 index 0000000..fc79747 --- /dev/null +++ b/frontend/src/app.d.ts @@ -0,0 +1,18 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + interface Locals { + user: import('$lib/server/auth').SessionValidationResult['user']; + session: import('$lib/server/auth').SessionValidationResult['session']; + } + + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/frontend/src/app.html b/frontend/src/app.html new file mode 100644 index 0000000..f273cc5 --- /dev/null +++ b/frontend/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/frontend/src/demo.spec.ts b/frontend/src/demo.spec.ts new file mode 100644 index 0000000..e07cbbd --- /dev/null +++ b/frontend/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/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts new file mode 100644 index 0000000..3c37e24 --- /dev/null +++ b/frontend/src/hooks.server.ts @@ -0,0 +1,28 @@ +import type { Handle } from '@sveltejs/kit'; +import * as auth from '$lib/server/auth'; + +const handleAuth: Handle = async ({ event, resolve }) => { + const sessionToken = event.cookies.get(auth.sessionCookieName); + + if (!sessionToken) { + event.locals.user = null; + event.locals.session = null; + + return resolve(event); + } + + const { session, user } = await auth.validateSessionToken(sessionToken); + + if (session) { + auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); + } else { + auth.deleteSessionTokenCookie(event); + } + + event.locals.user = user; + event.locals.session = session; + + return resolve(event); +}; + +export const handle: Handle = handleAuth; diff --git a/frontend/src/lib/assets/favicon.svg b/frontend/src/lib/assets/favicon.svg new file mode 100644 index 0000000..cc5dc66 --- /dev/null +++ b/frontend/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/frontend/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/frontend/src/lib/server/auth.ts b/frontend/src/lib/server/auth.ts new file mode 100644 index 0000000..38c9930 --- /dev/null +++ b/frontend/src/lib/server/auth.ts @@ -0,0 +1,81 @@ +import type { RequestEvent } from '@sveltejs/kit'; +import { eq } from 'drizzle-orm'; +import { sha256 } from '@oslojs/crypto/sha2'; +import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding'; +import { db } from '$lib/server/db'; +import * as table from '$lib/server/db/schema'; + +const DAY_IN_MS = 1000 * 60 * 60 * 24; + +export const sessionCookieName = 'auth-session'; + +export function generateSessionToken() { + const bytes = crypto.getRandomValues(new Uint8Array(18)); + const token = encodeBase64url(bytes); + return token; +} + +export async function createSession(token: string, userId: string) { + const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + const session: table.Session = { + id: sessionId, + userId, + expiresAt: new Date(Date.now() + DAY_IN_MS * 30) + }; + await db.insert(table.session).values(session); + return session; +} + +export async function validateSessionToken(token: string) { + const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + const [result] = await db + .select({ + // Adjust user table here to tweak returned data + user: { id: table.user.id, username: table.user.username }, + session: table.session + }) + .from(table.session) + .innerJoin(table.user, eq(table.session.userId, table.user.id)) + .where(eq(table.session.id, sessionId)); + + if (!result) { + return { session: null, user: null }; + } + const { session, user } = result; + + const sessionExpired = Date.now() >= session.expiresAt.getTime(); + if (sessionExpired) { + await db.delete(table.session).where(eq(table.session.id, session.id)); + return { session: null, user: null }; + } + + const renewSession = Date.now() >= session.expiresAt.getTime() - DAY_IN_MS * 15; + if (renewSession) { + session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30); + await db + .update(table.session) + .set({ expiresAt: session.expiresAt }) + .where(eq(table.session.id, session.id)); + } + + return { session, user }; +} + +export type SessionValidationResult = Awaited>; + +export async function invalidateSession(sessionId: string) { + await db.delete(table.session).where(eq(table.session.id, sessionId)); +} + +export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date) { + event.cookies.set(sessionCookieName, token, { + expires: expiresAt, + path: '/' + }); +} + +export function deleteSessionTokenCookie(event: RequestEvent) { + event.cookies.delete(sessionCookieName, { + path: '/' + }); +} diff --git a/frontend/src/lib/server/db/index.ts b/frontend/src/lib/server/db/index.ts new file mode 100644 index 0000000..fb8b355 --- /dev/null +++ b/frontend/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/frontend/src/lib/server/db/schema.ts b/frontend/src/lib/server/db/schema.ts new file mode 100644 index 0000000..cbb6d1a --- /dev/null +++ b/frontend/src/lib/server/db/schema.ts @@ -0,0 +1,20 @@ +import { pgTable, serial, integer, text, timestamp } from 'drizzle-orm/pg-core'; + +export const user = pgTable('user', { + id: text('id').primaryKey(), + age: integer('age'), + username: text('username').notNull().unique(), + passwordHash: text('password_hash').notNull() +}); + +export const session = pgTable('session', { + id: text('id').primaryKey(), + userId: text('user_id') + .notNull() + .references(() => user.id), + expiresAt: timestamp('expires_at', { withTimezone: true, mode: 'date' }).notNull() +}); + +export type Session = typeof session.$inferSelect; + +export type User = typeof user.$inferSelect; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..0d8eb03 --- /dev/null +++ b/frontend/src/routes/+layout.svelte @@ -0,0 +1,9 @@ + + + +{@render children()} diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte new file mode 100644 index 0000000..cc88df0 --- /dev/null +++ b/frontend/src/routes/+page.svelte @@ -0,0 +1,2 @@ +

Welcome to SvelteKit

+

Visit svelte.dev/docs/kit to read the documentation

diff --git a/frontend/src/routes/demo/+page.svelte b/frontend/src/routes/demo/+page.svelte new file mode 100644 index 0000000..501e687 --- /dev/null +++ b/frontend/src/routes/demo/+page.svelte @@ -0,0 +1,5 @@ + + +lucia diff --git a/frontend/src/routes/demo/lucia/+page.server.ts b/frontend/src/routes/demo/lucia/+page.server.ts new file mode 100644 index 0000000..9fe83e5 --- /dev/null +++ b/frontend/src/routes/demo/lucia/+page.server.ts @@ -0,0 +1,31 @@ +import * as auth from '$lib/server/auth'; +import { fail, redirect } from '@sveltejs/kit'; +import { getRequestEvent } from '$app/server'; +import type { Actions, PageServerLoad } from './$types'; + +export const load: PageServerLoad = async () => { + const user = requireLogin(); + return { user }; +}; + +export const actions: Actions = { + logout: async (event) => { + if (!event.locals.session) { + return fail(401); + } + await auth.invalidateSession(event.locals.session.id); + auth.deleteSessionTokenCookie(event); + + return redirect(302, '/demo/lucia/login'); + } +}; + +function requireLogin() { + const { locals } = getRequestEvent(); + + if (!locals.user) { + return redirect(302, '/demo/lucia/login'); + } + + return locals.user; +} diff --git a/frontend/src/routes/demo/lucia/+page.svelte b/frontend/src/routes/demo/lucia/+page.svelte new file mode 100644 index 0000000..cefb2d1 --- /dev/null +++ b/frontend/src/routes/demo/lucia/+page.svelte @@ -0,0 +1,12 @@ + + +

Hi, {data.user.username}!

+

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

+
+ +
diff --git a/frontend/src/routes/demo/lucia/login/+page.server.ts b/frontend/src/routes/demo/lucia/login/+page.server.ts new file mode 100644 index 0000000..97d1235 --- /dev/null +++ b/frontend/src/routes/demo/lucia/login/+page.server.ts @@ -0,0 +1,107 @@ +import { hash, verify } from '@node-rs/argon2'; +import { encodeBase32LowerCase } from '@oslojs/encoding'; +import { fail, redirect } from '@sveltejs/kit'; +import { eq } from 'drizzle-orm'; +import * as auth from '$lib/server/auth'; +import { db } from '$lib/server/db'; +import * as table from '$lib/server/db/schema'; +import type { Actions, PageServerLoad } from './$types'; + +export const load: PageServerLoad = async (event) => { + if (event.locals.user) { + return redirect(302, '/demo/lucia'); + } + return {}; +}; + +export const actions: Actions = { + login: async (event) => { + const formData = await event.request.formData(); + const username = formData.get('username'); + const password = formData.get('password'); + + if (!validateUsername(username)) { + return fail(400, { + message: 'Invalid username (min 3, max 31 characters, alphanumeric only)' + }); + } + if (!validatePassword(password)) { + return fail(400, { message: 'Invalid password (min 6, max 255 characters)' }); + } + + const results = await db.select().from(table.user).where(eq(table.user.username, username)); + + const existingUser = results.at(0); + if (!existingUser) { + return fail(400, { message: 'Incorrect username or password' }); + } + + const validPassword = await verify(existingUser.passwordHash, password, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1 + }); + if (!validPassword) { + return fail(400, { message: 'Incorrect username or password' }); + } + + const sessionToken = auth.generateSessionToken(); + const session = await auth.createSession(sessionToken, existingUser.id); + auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); + + return redirect(302, '/demo/lucia'); + }, + register: async (event) => { + const formData = await event.request.formData(); + const username = formData.get('username'); + const password = formData.get('password'); + + if (!validateUsername(username)) { + return fail(400, { message: 'Invalid username' }); + } + if (!validatePassword(password)) { + return fail(400, { message: 'Invalid password' }); + } + + const userId = generateUserId(); + const passwordHash = await hash(password, { + // recommended minimum parameters + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1 + }); + + try { + await db.insert(table.user).values({ id: userId, username, passwordHash }); + + const sessionToken = auth.generateSessionToken(); + const session = await auth.createSession(sessionToken, userId); + auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); + } catch { + return fail(500, { message: 'An error has occurred' }); + } + return redirect(302, '/demo/lucia'); + } +}; + +function generateUserId() { + // ID with 120 bits of entropy, or about the same as UUID v4. + const bytes = crypto.getRandomValues(new Uint8Array(15)); + const id = encodeBase32LowerCase(bytes); + return id; +} + +function validateUsername(username: unknown): username is string { + return ( + typeof username === 'string' && + username.length >= 3 && + username.length <= 31 && + /^[a-z0-9_-]+$/.test(username) + ); +} + +function validatePassword(password: unknown): password is string { + return typeof password === 'string' && password.length >= 6 && password.length <= 255; +} diff --git a/frontend/src/routes/demo/lucia/login/+page.svelte b/frontend/src/routes/demo/lucia/login/+page.svelte new file mode 100644 index 0000000..39c8037 --- /dev/null +++ b/frontend/src/routes/demo/lucia/login/+page.svelte @@ -0,0 +1,34 @@ + + +

Login/Register

+
+ + + + +
+

{form?.message ?? ''}

diff --git a/frontend/src/routes/layout.css b/frontend/src/routes/layout.css new file mode 100644 index 0000000..cd67023 --- /dev/null +++ b/frontend/src/routes/layout.css @@ -0,0 +1,3 @@ +@import 'tailwindcss'; +@plugin '@tailwindcss/forms'; +@plugin '@tailwindcss/typography'; diff --git a/frontend/src/routes/page.svelte.spec.ts b/frontend/src/routes/page.svelte.spec.ts new file mode 100644 index 0000000..d54f00a --- /dev/null +++ b/frontend/src/routes/page.svelte.spec.ts @@ -0,0 +1,13 @@ +import { page } from 'vitest/browser'; +import { describe, expect, it } from 'vitest'; +import { render } from 'vitest-browser-svelte'; +import Page from './+page.svelte'; + +describe('/+page.svelte', () => { + it('should render h1', async () => { + render(Page); + + const heading = page.getByRole('heading', { level: 1 }); + await expect.element(heading).toBeInTheDocument(); + }); +}); diff --git a/frontend/static/robots.txt b/frontend/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/frontend/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js new file mode 100644 index 0000000..e744595 --- /dev/null +++ b/frontend/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/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..2c2ed3c --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..c4ffa6e --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,43 @@ +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}'] + } + } + ] + } +}); 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 deleted file mode 100644 index a1abd3a..0000000 --- a/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "esports", - "module": "index.ts", - "type": "module", - "private": true, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5" - } -} From 8f49b73aa83efa12c644740b6515f3bb594e25ec Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Fri, 27 Feb 2026 14:56:58 +0100 Subject: [PATCH 02/14] restart from scratch (weekend mvp) --- backend/.env.example | 19 - backend/.gitignore | 34 - backend/CLAUDE.md | 106 --- backend/README.md | 15 - backend/bun.lock | 75 -- backend/package.json | 23 - backend/src/application/events/EventBus.ts | 56 -- backend/src/application/events/index.ts | 1 - .../src/application/services/MatchService.ts | 334 ------- .../src/application/services/QueueService.ts | 183 ---- backend/src/application/services/index.ts | 2 - backend/src/cli/index.ts | 262 ------ backend/src/domain/events/DomainEvent.ts | 13 - backend/src/domain/events/MatchEvents.ts | 115 --- backend/src/domain/events/QueueEvents.ts | 35 - backend/src/domain/events/index.ts | 3 - backend/src/domain/interfaces/IGameService.ts | 18 - .../src/domain/interfaces/IMatchProtocol.ts | 0 .../src/domain/interfaces/IMatchRepository.ts | 12 - .../domain/interfaces/INotificationService.ts | 11 - .../domain/interfaces/IPlayerRepository.ts | 10 - .../src/domain/interfaces/IQueueRepository.ts | 15 - .../src/domain/interfaces/ITeamRepository.ts | 9 - backend/src/domain/interfaces/index.ts | 7 - backend/src/domain/models/Match.ts | 71 -- backend/src/domain/models/MatchState.ts | 20 - backend/src/domain/models/OsuMap.ts | 45 - backend/src/domain/models/Player.ts | 29 - backend/src/domain/models/QueueEntry.ts | 22 - backend/src/domain/models/Score.ts | 38 - backend/src/domain/models/Team.ts | 35 - backend/src/domain/models/index.ts | 7 - .../domain/protocols/HeadToHeadProtocol.ts | 160 ---- .../src/domain/protocols/IMatchProtocol.ts | 63 -- .../src/domain/protocols/ProtocolRegistry.ts | 32 - .../domain/protocols/StandardOsuProtocol.ts | 192 ---- backend/src/domain/protocols/index.ts | 4 - backend/src/index.ts | 197 ---- .../database/PostgresMatchRepository.ts | 293 ------ .../database/PostgresPlayerRepository.ts | 77 -- .../database/PostgresQueueRepository.ts | 173 ---- .../database/PostgresTeamRepository.ts | 128 --- backend/src/infrastructure/database/index.ts | 4 - .../database/migrations/001_initial.sql | 98 -- .../infrastructure/discord/DiscordService.ts | 174 ---- backend/src/infrastructure/discord/index.ts | 1 - .../mocks/InMemoryMatchRepository.ts | 63 -- .../mocks/InMemoryPlayerRepository.ts | 47 - .../mocks/InMemoryQueueRepository.ts | 61 -- .../mocks/InMemoryTeamRepository.ts | 36 - .../infrastructure/mocks/MockGameService.ts | 136 --- .../mocks/MockNotificationService.ts | 50 - backend/src/infrastructure/mocks/index.ts | 6 - backend/src/infrastructure/osu/OsuService.ts | 187 ---- backend/src/infrastructure/osu/index.ts | 1 - .../websocket/WebSocketService.ts | 220 ----- backend/src/infrastructure/websocket/index.ts | 1 - backend/src/presentation/api/routes.ts | 0 backend/src/testing/TestHarness.ts | 217 ----- backend/src/testing/index.ts | 1 - .../tests/application/QueueService.test.ts | 97 -- .../protocols/StandardOsuProtocol.test.ts | 155 ---- .../tests/integration/full-match-flow.test.ts | 121 --- backend/tsconfig.json | 29 - frontend/.env.example | 2 - frontend/.gitignore | 24 - frontend/.npmrc | 1 - frontend/.prettierignore | 10 - frontend/.prettierrc | 16 - frontend/.vscode/settings.json | 5 - frontend/README.md | 42 - frontend/bun.lock | 854 ------------------ frontend/drizzle.config.ts | 11 - frontend/e2e/demo.test.ts | 6 - frontend/eslint.config.js | 41 - frontend/package.json | 61 -- frontend/playwright.config.ts | 6 - frontend/src/app.d.ts | 18 - frontend/src/app.html | 11 - frontend/src/demo.spec.ts | 7 - frontend/src/hooks.server.ts | 28 - frontend/src/lib/assets/favicon.svg | 1 - frontend/src/lib/index.ts | 1 - frontend/src/lib/server/auth.ts | 81 -- frontend/src/lib/server/db/index.ts | 10 - frontend/src/lib/server/db/schema.ts | 20 - frontend/src/routes/+layout.svelte | 9 - frontend/src/routes/+page.svelte | 2 - frontend/src/routes/demo/+page.svelte | 5 - .../src/routes/demo/lucia/+page.server.ts | 31 - frontend/src/routes/demo/lucia/+page.svelte | 12 - .../routes/demo/lucia/login/+page.server.ts | 107 --- .../src/routes/demo/lucia/login/+page.svelte | 34 - frontend/src/routes/layout.css | 3 - frontend/src/routes/page.svelte.spec.ts | 13 - frontend/static/robots.txt | 3 - frontend/svelte.config.js | 6 - frontend/tsconfig.json | 20 - frontend/vite.config.ts | 43 - 99 files changed, 6193 deletions(-) delete mode 100644 backend/.env.example delete mode 100644 backend/.gitignore delete mode 100644 backend/CLAUDE.md delete mode 100644 backend/README.md delete mode 100644 backend/bun.lock delete mode 100644 backend/package.json delete mode 100644 backend/src/application/events/EventBus.ts delete mode 100644 backend/src/application/events/index.ts delete mode 100644 backend/src/application/services/MatchService.ts delete mode 100644 backend/src/application/services/QueueService.ts delete mode 100644 backend/src/application/services/index.ts delete mode 100644 backend/src/cli/index.ts delete mode 100644 backend/src/domain/events/DomainEvent.ts delete mode 100644 backend/src/domain/events/MatchEvents.ts delete mode 100644 backend/src/domain/events/QueueEvents.ts delete mode 100644 backend/src/domain/events/index.ts delete mode 100644 backend/src/domain/interfaces/IGameService.ts delete mode 100644 backend/src/domain/interfaces/IMatchProtocol.ts delete mode 100644 backend/src/domain/interfaces/IMatchRepository.ts delete mode 100644 backend/src/domain/interfaces/INotificationService.ts delete mode 100644 backend/src/domain/interfaces/IPlayerRepository.ts delete mode 100644 backend/src/domain/interfaces/IQueueRepository.ts delete mode 100644 backend/src/domain/interfaces/ITeamRepository.ts delete mode 100644 backend/src/domain/interfaces/index.ts delete mode 100644 backend/src/domain/models/Match.ts delete mode 100644 backend/src/domain/models/MatchState.ts delete mode 100644 backend/src/domain/models/OsuMap.ts delete mode 100644 backend/src/domain/models/Player.ts delete mode 100644 backend/src/domain/models/QueueEntry.ts delete mode 100644 backend/src/domain/models/Score.ts delete mode 100644 backend/src/domain/models/Team.ts delete mode 100644 backend/src/domain/models/index.ts delete mode 100644 backend/src/domain/protocols/HeadToHeadProtocol.ts delete mode 100644 backend/src/domain/protocols/IMatchProtocol.ts delete mode 100644 backend/src/domain/protocols/ProtocolRegistry.ts delete mode 100644 backend/src/domain/protocols/StandardOsuProtocol.ts delete mode 100644 backend/src/domain/protocols/index.ts delete mode 100644 backend/src/index.ts delete mode 100644 backend/src/infrastructure/database/PostgresMatchRepository.ts delete mode 100644 backend/src/infrastructure/database/PostgresPlayerRepository.ts delete mode 100644 backend/src/infrastructure/database/PostgresQueueRepository.ts delete mode 100644 backend/src/infrastructure/database/PostgresTeamRepository.ts delete mode 100644 backend/src/infrastructure/database/index.ts delete mode 100644 backend/src/infrastructure/database/migrations/001_initial.sql delete mode 100644 backend/src/infrastructure/discord/DiscordService.ts delete mode 100644 backend/src/infrastructure/discord/index.ts delete mode 100644 backend/src/infrastructure/mocks/InMemoryMatchRepository.ts delete mode 100644 backend/src/infrastructure/mocks/InMemoryPlayerRepository.ts delete mode 100644 backend/src/infrastructure/mocks/InMemoryQueueRepository.ts delete mode 100644 backend/src/infrastructure/mocks/InMemoryTeamRepository.ts delete mode 100644 backend/src/infrastructure/mocks/MockGameService.ts delete mode 100644 backend/src/infrastructure/mocks/MockNotificationService.ts delete mode 100644 backend/src/infrastructure/mocks/index.ts delete mode 100644 backend/src/infrastructure/osu/OsuService.ts delete mode 100644 backend/src/infrastructure/osu/index.ts delete mode 100644 backend/src/infrastructure/websocket/WebSocketService.ts delete mode 100644 backend/src/infrastructure/websocket/index.ts delete mode 100644 backend/src/presentation/api/routes.ts delete mode 100644 backend/src/testing/TestHarness.ts delete mode 100644 backend/src/testing/index.ts delete mode 100644 backend/tests/application/QueueService.test.ts delete mode 100644 backend/tests/domain/protocols/StandardOsuProtocol.test.ts delete mode 100644 backend/tests/integration/full-match-flow.test.ts delete mode 100644 backend/tsconfig.json delete mode 100644 frontend/.env.example delete mode 100644 frontend/.gitignore delete mode 100644 frontend/.npmrc delete mode 100644 frontend/.prettierignore delete mode 100644 frontend/.prettierrc delete mode 100644 frontend/.vscode/settings.json delete mode 100644 frontend/README.md delete mode 100644 frontend/bun.lock delete mode 100644 frontend/drizzle.config.ts delete mode 100644 frontend/e2e/demo.test.ts delete mode 100644 frontend/eslint.config.js delete mode 100644 frontend/package.json delete mode 100644 frontend/playwright.config.ts delete mode 100644 frontend/src/app.d.ts delete mode 100644 frontend/src/app.html delete mode 100644 frontend/src/demo.spec.ts delete mode 100644 frontend/src/hooks.server.ts delete mode 100644 frontend/src/lib/assets/favicon.svg delete mode 100644 frontend/src/lib/index.ts delete mode 100644 frontend/src/lib/server/auth.ts delete mode 100644 frontend/src/lib/server/db/index.ts delete mode 100644 frontend/src/lib/server/db/schema.ts delete mode 100644 frontend/src/routes/+layout.svelte delete mode 100644 frontend/src/routes/+page.svelte delete mode 100644 frontend/src/routes/demo/+page.svelte delete mode 100644 frontend/src/routes/demo/lucia/+page.server.ts delete mode 100644 frontend/src/routes/demo/lucia/+page.svelte delete mode 100644 frontend/src/routes/demo/lucia/login/+page.server.ts delete mode 100644 frontend/src/routes/demo/lucia/login/+page.svelte delete mode 100644 frontend/src/routes/layout.css delete mode 100644 frontend/src/routes/page.svelte.spec.ts delete mode 100644 frontend/static/robots.txt delete mode 100644 frontend/svelte.config.js delete mode 100644 frontend/tsconfig.json delete mode 100644 frontend/vite.config.ts diff --git a/backend/.env.example b/backend/.env.example deleted file mode 100644 index 46e2b23..0000000 --- a/backend/.env.example +++ /dev/null @@ -1,19 +0,0 @@ -# Server Configuration -PORT=3000 - -# Database (PostgreSQL) -DATABASE_URL=postgres://user:password@localhost:5432/vash_esports - -# Discord Bot -DISCORD_TOKEN=your_discord_bot_token -DISCORD_GUILD_ID=your_guild_id -DISCORD_ANNOUNCEMENT_CHANNEL_ID=your_channel_id -DISCORD_MATCH_RESULTS_CHANNEL_ID=your_channel_id - -# osu! API -OSU_CLIENT_ID=your_client_id -OSU_CLIENT_SECRET=your_client_secret - -# osu! IRC (for BanchoBot) -OSU_IRC_USERNAME=your_osu_username -OSU_IRC_PASSWORD=your_irc_password diff --git a/backend/.gitignore b/backend/.gitignore deleted file mode 100644 index a14702c..0000000 --- a/backend/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# dependencies (bun install) -node_modules - -# output -out -dist -*.tgz - -# code coverage -coverage -*.lcov - -# logs -logs -_.log -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# caches -.eslintcache -.cache -*.tsbuildinfo - -# IntelliJ based IDEs -.idea - -# Finder (MacOS) folder config -.DS_Store diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md deleted file mode 100644 index 1ee6890..0000000 --- a/backend/CLAUDE.md +++ /dev/null @@ -1,106 +0,0 @@ - -Default to using Bun instead of Node.js. - -- Use `bun ` instead of `node ` or `ts-node ` -- Use `bun test` instead of `jest` or `vitest` -- Use `bun build ` instead of `webpack` or `esbuild` -- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` -- Use `bun run - - -``` - -With the following `frontend.tsx`: - -```tsx#frontend.tsx -import React from "react"; - -// import .css files directly and it works -import './index.css'; - -import { createRoot } from "react-dom/client"; - -const root = createRoot(document.body); - -export default function Frontend() { - return

Hello, world!

; -} - -root.render(); -``` - -Then, run index.ts - -```sh -bun --hot ./index.ts -``` - -For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index 3dd5e6d..0000000 --- a/backend/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# backend - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v1.3.3. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/backend/bun.lock b/backend/bun.lock deleted file mode 100644 index dab0933..0000000 --- a/backend/bun.lock +++ /dev/null @@ -1,75 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "backend", - "dependencies": { - "discord.js": "^14.14.1", - }, - "devDependencies": { - "@types/bun": "latest", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, - }, - "packages": { - "@discordjs/builders": ["@discordjs/builders@1.13.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.33", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w=="], - - "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], - - "@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="], - - "@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="], - - "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="], - - "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], - - "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], - - "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], - - "@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="], - - "@types/bun": ["@types/bun@1.3.7", "", { "dependencies": { "bun-types": "1.3.7" } }, "sha512-lmNuMda+Z9b7tmhA0tohwy8ZWFSnmQm1UDWXtH5r9F7wZCfkeO3Jx7wKQ1EOiKq43yHts7ky6r8SDJQWRNupkA=="], - - "@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="], - - "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - - "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], - - "bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="], - - "discord-api-types": ["discord-api-types@0.38.38", "", {}, "sha512-7qcM5IeZrfb+LXW07HvoI5L+j4PQeMZXEkSm1htHAHh4Y9JSMXBWjy/r7zmUCOj4F7zNjMcm7IMWr131MT2h0Q=="], - - "discord.js": ["discord.js@14.25.1", "", { "dependencies": { "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.33", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], - - "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], - - "magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="], - - "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], - - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "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=="], - - "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], - - "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], - } -} diff --git a/backend/package.json b/backend/package.json deleted file mode 100644 index 5ad313b..0000000 --- a/backend/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "vash-esports-backend", - "version": "1.0.0", - "module": "src/index.ts", - "type": "module", - "private": true, - "scripts": { - "dev": "bun --hot src/index.ts", - "start": "bun src/index.ts", - "test": "bun test", - "cli": "bun src/cli/index.ts", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "discord.js": "^14.14.1" - }, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5" - } -} diff --git a/backend/src/application/events/EventBus.ts b/backend/src/application/events/EventBus.ts deleted file mode 100644 index 6691ac0..0000000 --- a/backend/src/application/events/EventBus.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { DomainEvent } from "../../domain/events/DomainEvent"; - -type EventHandler = (event: T) => void | Promise; - -export class EventBus { - private handlers: Map> = new Map(); - private allHandlers: Set = new Set(); - - subscribe( - type: T["type"], - handler: EventHandler - ): () => void { - if (!this.handlers.has(type)) { - this.handlers.set(type, new Set()); - } - this.handlers.get(type)!.add(handler as EventHandler); - - return () => { - this.handlers.get(type)?.delete(handler as EventHandler); - }; - } - - subscribeAll(handler: EventHandler): () => void { - this.allHandlers.add(handler); - return () => { - this.allHandlers.delete(handler); - }; - } - - async publish(event: DomainEvent): Promise { - const handlers = this.handlers.get(event.type) ?? new Set(); - - const allPromises: Promise[] = []; - - for (const handler of handlers) { - const result = handler(event); - if (result instanceof Promise) { - allPromises.push(result); - } - } - - for (const handler of this.allHandlers) { - const result = handler(event); - if (result instanceof Promise) { - allPromises.push(result); - } - } - - await Promise.all(allPromises); - } - - clear(): void { - this.handlers.clear(); - this.allHandlers.clear(); - } -} diff --git a/backend/src/application/events/index.ts b/backend/src/application/events/index.ts deleted file mode 100644 index ac9eb5d..0000000 --- a/backend/src/application/events/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./EventBus"; diff --git a/backend/src/application/services/MatchService.ts b/backend/src/application/services/MatchService.ts deleted file mode 100644 index 1743bca..0000000 --- a/backend/src/application/services/MatchService.ts +++ /dev/null @@ -1,334 +0,0 @@ -import type { IMatchRepository } from "../../domain/interfaces/IMatchRepository"; -import type { IPlayerRepository } from "../../domain/interfaces/IPlayerRepository"; -import type { IGameService } from "../../domain/interfaces/IGameService"; -import type { INotificationService } from "../../domain/interfaces/INotificationService"; -import type { ProtocolRegistry } from "../../domain/protocols/ProtocolRegistry"; -import type { ProtocolContext, RollResult } from "../../domain/protocols/IMatchProtocol"; -import type { EventBus } from "../events/EventBus"; -import type { Team } from "../../domain/models/Team"; -import type { Player } from "../../domain/models/Player"; -import type { OsuMap } from "../../domain/models/OsuMap"; -import type { Score } from "../../domain/models/Score"; -import { type Match, createMatch, allPlayersJoined, getTeamWins } from "../../domain/models/Match"; -import { MatchState } from "../../domain/models/MatchState"; -import { createMatchScore } from "../../domain/models/Score"; -import { MatchEvents } from "../../domain/events/MatchEvents"; - -export class MatchService { - constructor( - private matchRepo: IMatchRepository, - private playerRepo: IPlayerRepository, - private protocolRegistry: ProtocolRegistry, - private gameService: IGameService, - private notificationService: INotificationService, - private eventBus: EventBus - ) { - this.setupGameServiceCallbacks(); - } - - private setupGameServiceCallbacks(): void { - this.gameService.onPlayerJoin(async (osuId, lobbyId) => { - const match = await this.matchRepo.findByLobbyId(lobbyId); - if (!match) return; - - const player = await this.playerRepo.findByOsuId(osuId); - if (!player) return; - - await this.handlePlayerJoined(match.id, player); - }); - - this.gameService.onRoll(async (osuId, lobbyId, roll) => { - const match = await this.matchRepo.findByLobbyId(lobbyId); - if (!match) return; - - const player = await this.playerRepo.findByOsuId(osuId); - if (!player) return; - - await this.handleRoll(match.id, player, roll); - }); - - this.gameService.onMatchFinish(async (lobbyId, scores) => { - const match = await this.matchRepo.findByLobbyId(lobbyId); - if (!match) return; - - await this.handleMapComplete(match.id, scores); - }); - } - - async createMatch(teams: Team[], protocolName: string): Promise { - const protocol = this.protocolRegistry.get(protocolName); - - const match = createMatch({ - teams, - protocol: protocolName, - }); - - await this.matchRepo.save(match); - await this.eventBus.publish(MatchEvents.created(match)); - - return match; - } - - async startMatch(matchId: string): Promise { - const match = await this.matchRepo.findById(matchId); - if (!match) { - throw new Error(`Match ${matchId} not found`); - } - - const protocol = this.protocolRegistry.get(match.protocol); - const context = this.createProtocolContext(match); - - const lobbyId = await this.gameService.createLobby(match); - match.lobbyId = lobbyId; - match.startedAt = new Date(); - - const newState = await protocol.onMatchStart(match, context); - if (newState) { - await this.transitionState(match, newState); - } - - for (const team of match.teams) { - for (const player of team.players) { - await this.gameService.invitePlayer(player, lobbyId); - } - } - - await this.notificationService.announceMatch(match); - await this.matchRepo.save(match); - } - - async handlePlayerJoined(matchId: string, player: Player): Promise { - const match = await this.matchRepo.findById(matchId); - if (!match) return; - - if (match.state !== MatchState.WAITING_PLAYERS) return; - - const isMatchPlayer = match.teams.some((t) => - t.players.some((p) => p.id === player.id) - ); - if (!isMatchPlayer) return; - - match.joinedPlayers.add(player.id); - await this.eventBus.publish(MatchEvents.playerJoined(match, player)); - - if (allPlayersJoined(match)) { - const protocol = this.protocolRegistry.get(match.protocol); - const context = this.createProtocolContext(match); - - const newState = await protocol.onAllPlayersJoined(match, context); - if (newState) { - await this.transitionState(match, newState); - } - } - - await this.matchRepo.save(match); - } - - async handleRoll(matchId: string, player: Player, roll: number): Promise { - const match = await this.matchRepo.findById(matchId); - if (!match) return; - - if (match.state !== MatchState.ROLLING) return; - - const team = match.teams.find((t) => t.players.some((p) => p.id === player.id)); - if (!team) return; - - if (match.rolls.has(team.id)) return; - - match.rolls.set(team.id, roll); - await this.eventBus.publish(MatchEvents.rollCompleted(match, player, roll)); - - const rolls: RollResult[] = Array.from(match.rolls.entries()).map( - ([teamId, rollValue]) => ({ - teamId, - playerId: player.id, - roll: rollValue, - }) - ); - - if (rolls.length === match.teams.length) { - const protocol = this.protocolRegistry.get(match.protocol); - const context = this.createProtocolContext(match); - - const newState = await protocol.onRollComplete(match, rolls, context); - if (newState) { - await this.transitionState(match, newState); - } - } - - await this.matchRepo.save(match); - } - - async handlePick(matchId: string, team: Team, map: OsuMap): Promise { - const match = await this.matchRepo.findById(matchId); - if (!match) return; - - if (match.state !== MatchState.PICKING) return; - - const protocol = this.protocolRegistry.get(match.protocol); - if (!protocol.isValidPick(match, team, map)) return; - - const context = this.createProtocolContext(match); - await this.eventBus.publish(MatchEvents.mapPicked(match, team, map)); - - const newState = await protocol.onPick(match, team, map, context); - if (newState) { - await this.transitionState(match, newState); - } - - await this.matchRepo.save(match); - } - - async handleBan(matchId: string, team: Team, map: OsuMap): Promise { - const match = await this.matchRepo.findById(matchId); - if (!match) return; - - if (match.state !== MatchState.BANNING) return; - - const protocol = this.protocolRegistry.get(match.protocol); - if (!protocol.isValidBan(match, team, map)) return; - - const context = this.createProtocolContext(match); - await this.eventBus.publish(MatchEvents.mapBanned(match, team, map)); - - const newState = await protocol.onBan(match, team, map, context); - if (newState) { - await this.transitionState(match, newState); - } - - await this.matchRepo.save(match); - } - - async handleMapComplete(matchId: string, scores: Score[]): Promise { - const match = await this.matchRepo.findById(matchId); - if (!match) return; - - if (match.state !== MatchState.PLAYING) return; - - const protocol = this.protocolRegistry.get(match.protocol); - const winnerId = protocol.calculateMapWinner(match, scores); - - const teamScores = new Map(); - const playerScores = new Map(); - - for (const team of match.teams) { - const teamPlayerIds = team.players.map((p) => p.id); - const teamTotal = scores - .filter((s) => teamPlayerIds.includes(s.playerId)) - .reduce((sum, s) => sum + s.score, 0); - teamScores.set(team.id, teamTotal); - } - - for (const score of scores) { - playerScores.set(score.playerId, score); - } - - const currentMapId = match.picks[match.picks.length - 1]; - const matchScore = createMatchScore({ - mapId: currentMapId, - teamScores, - playerScores, - winner: winnerId, - }); - - match.scores.push(matchScore); - await this.eventBus.publish(MatchEvents.mapCompleted(match, matchScore)); - await this.notificationService.announceMapResult(match, matchScore); - - const context = this.createProtocolContext(match); - const newState = await protocol.onMapComplete(match, scores, context); - - if (newState === MatchState.COMPLETED) { - const winner = match.teams.find((t) => t.id === protocol.calculateMatchWinner(match)); - if (winner) { - match.winner = winner; - match.endedAt = new Date(); - await this.eventBus.publish(MatchEvents.completed(match, winner)); - await this.notificationService.announceMatchResult(match, winner); - await this.updatePlayerRatings(match); - } - } - - if (newState) { - await this.transitionState(match, newState); - } - - await this.matchRepo.save(match); - } - - async cancelMatch(matchId: string, reason: string): Promise { - const match = await this.matchRepo.findById(matchId); - if (!match) return; - - match.endedAt = new Date(); - await this.transitionState(match, MatchState.CANCELLED); - await this.eventBus.publish(MatchEvents.cancelled(match, reason)); - - if (match.lobbyId) { - await this.gameService.closeLobby(match.lobbyId); - } - - await this.matchRepo.save(match); - } - - private async transitionState(match: Match, newState: MatchState): Promise { - const previousState = match.state; - match.state = newState; - await this.matchRepo.updateState(match.id, newState); - await this.eventBus.publish(MatchEvents.stateChanged(match, previousState)); - } - - private createProtocolContext(match: Match): ProtocolContext { - return { - sendMessage: async (message: string) => { - if (match.lobbyId) { - await this.gameService.sendMessage(match.lobbyId, message); - } - }, - setMap: async (mapId: number) => { - if (match.lobbyId) { - await this.gameService.setMap(match.lobbyId, mapId); - } - }, - startGame: async () => { - if (match.lobbyId) { - await this.gameService.startMatch(match.lobbyId); - } - }, - }; - } - - private async updatePlayerRatings(match: Match): Promise { - if (!match.winner) return; - - const K = 32; - const winningTeam = match.winner; - const losingTeam = match.teams.find((t) => t.id !== winningTeam.id); - if (!losingTeam) return; - - const winnerAvgRating = - winningTeam.players.reduce((sum, p) => sum + p.rating, 0) / - winningTeam.players.length; - const loserAvgRating = - losingTeam.players.reduce((sum, p) => sum + p.rating, 0) / - losingTeam.players.length; - - const expectedWinner = - 1 / (1 + Math.pow(10, (loserAvgRating - winnerAvgRating) / 400)); - const expectedLoser = 1 - expectedWinner; - - const winnerDelta = Math.round(K * (1 - expectedWinner)); - const loserDelta = Math.round(K * (0 - expectedLoser)); - - for (const player of winningTeam.players) { - await this.playerRepo.updateRating(player.id, player.rating + winnerDelta); - } - - for (const player of losingTeam.players) { - await this.playerRepo.updateRating( - player.id, - Math.max(100, player.rating + loserDelta) - ); - } - } -} diff --git a/backend/src/application/services/QueueService.ts b/backend/src/application/services/QueueService.ts deleted file mode 100644 index 9eb7142..0000000 --- a/backend/src/application/services/QueueService.ts +++ /dev/null @@ -1,183 +0,0 @@ -import type { IQueueRepository } from "../../domain/interfaces/IQueueRepository"; -import type { MatchService } from "./MatchService"; -import type { EventBus } from "../events/EventBus"; -import type { Team } from "../../domain/models/Team"; -import type { Match } from "../../domain/models/Match"; -import { type QueueEntry, createQueueEntry } from "../../domain/models/QueueEntry"; -import { getAverageRating } from "../../domain/models/Team"; -import { QueueEvents } from "../../domain/events/QueueEvents"; - -export interface MatchmakingConfig { - maxRatingDifference: number; - ratingExpansionRate: number; - minQueueTime: number; - protocol: string; -} - -const DEFAULT_CONFIG: MatchmakingConfig = { - maxRatingDifference: 200, - ratingExpansionRate: 50, - minQueueTime: 10000, - protocol: "standard-osu", -}; - -export class QueueService { - private config: MatchmakingConfig; - private matchmakingInterval?: ReturnType; - - constructor( - private queueRepo: IQueueRepository, - private matchService: MatchService, - private eventBus: EventBus, - config?: Partial - ) { - this.config = { ...DEFAULT_CONFIG, ...config }; - } - - async joinQueue(team: Team, queueType: string): Promise { - const existing = await this.queueRepo.findByTeamId(team.id); - if (existing) { - throw new Error(`Team ${team.id} is already in queue`); - } - - const entry = createQueueEntry({ - team, - queueType, - }); - - await this.queueRepo.add(entry); - await this.eventBus.publish(QueueEvents.teamJoined(entry)); - - return entry; - } - - async leaveQueue(teamId: string): Promise { - const entry = await this.queueRepo.findByTeamId(teamId); - if (!entry) { - throw new Error(`Team ${teamId} is not in queue`); - } - - await this.queueRepo.remove(teamId); - await this.eventBus.publish(QueueEvents.teamLeft(teamId, entry.queueType)); - } - - async findMatch(queueType: string): Promise { - const entries = await this.queueRepo.findByType(queueType); - if (entries.length < 2) { - return null; - } - - const matchPair = this.findBestMatch(entries); - if (!matchPair) { - return null; - } - - const [entry1, entry2] = matchPair; - - await this.queueRepo.remove(entry1.team.id); - await this.queueRepo.remove(entry2.team.id); - - const teams = [entry1.team, entry2.team]; - await this.eventBus.publish(QueueEvents.matchFound(teams, queueType)); - - const match = await this.matchService.createMatch(teams, this.config.protocol); - await this.matchService.startMatch(match.id); - - return match; - } - - async runMatchmaking(): Promise { - const queueTypes = new Set(); - const allEntries = await this.queueRepo.findAll(); - - for (const entry of allEntries) { - queueTypes.add(entry.queueType); - } - - for (const queueType of queueTypes) { - await this.findMatch(queueType); - } - } - - startAutoMatchmaking(intervalMs: number = 5000): void { - if (this.matchmakingInterval) { - clearInterval(this.matchmakingInterval); - } - this.matchmakingInterval = setInterval(() => { - this.runMatchmaking().catch(console.error); - }, intervalMs); - } - - stopAutoMatchmaking(): void { - if (this.matchmakingInterval) { - clearInterval(this.matchmakingInterval); - this.matchmakingInterval = undefined; - } - } - - private findBestMatch(entries: QueueEntry[]): [QueueEntry, QueueEntry] | null { - const now = Date.now(); - let bestPair: [QueueEntry, QueueEntry] | null = null; - let bestScore = Infinity; - - for (let i = 0; i < entries.length; i++) { - for (let j = i + 1; j < entries.length; j++) { - const entry1 = entries[i]; - const entry2 = entries[j]; - - if (!entry1 || !entry2) continue; - - const rating1 = getAverageRating(entry1.team); - const rating2 = getAverageRating(entry2.team); - const ratingDiff = Math.abs(rating1 - rating2); - - const waitTime1 = now - entry1.joinedAt.getTime(); - const waitTime2 = now - entry2.joinedAt.getTime(); - const minWaitTime = Math.min(waitTime1, waitTime2); - - if (minWaitTime < this.config.minQueueTime) { - continue; - } - - const expansionMultiplier = Math.floor(minWaitTime / this.config.minQueueTime); - const maxAllowedDiff = - this.config.maxRatingDifference + - expansionMultiplier * this.config.ratingExpansionRate; - - if (ratingDiff > maxAllowedDiff) { - continue; - } - - const score = ratingDiff - minWaitTime / 1000; - - if (score < bestScore) { - bestScore = score; - bestPair = [entry1, entry2]; - } - } - } - - return bestPair; - } - - async getQueueStatus(queueType: string): Promise<{ - count: number; - entries: Array<{ - teamName: string; - rating: number; - waitTime: number; - }>; - }> { - const entries = await this.queueRepo.findByType(queueType); - const now = Date.now(); - - return { - count: entries.length, - entries: entries.map((e) => ({ - teamName: e.team.name, - rating: getAverageRating(e.team), - waitTime: now - e.joinedAt.getTime(), - })), - }; - } -} diff --git a/backend/src/application/services/index.ts b/backend/src/application/services/index.ts deleted file mode 100644 index e35ce21..0000000 --- a/backend/src/application/services/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./MatchService"; -export * from "./QueueService"; diff --git a/backend/src/cli/index.ts b/backend/src/cli/index.ts deleted file mode 100644 index 19bfe29..0000000 --- a/backend/src/cli/index.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { parseArgs } from "util"; -import { EventBus } from "../application/events/EventBus"; -import { MatchService } from "../application/services/MatchService"; -import { QueueService } from "../application/services/QueueService"; -import { ProtocolRegistry } from "../domain/protocols/ProtocolRegistry"; -import { StandardOsuProtocol } from "../domain/protocols/StandardOsuProtocol"; -import { HeadToHeadProtocol } from "../domain/protocols/HeadToHeadProtocol"; -import { InMemoryPlayerRepository } from "../infrastructure/mocks/InMemoryPlayerRepository"; -import { InMemoryTeamRepository } from "../infrastructure/mocks/InMemoryTeamRepository"; -import { InMemoryMatchRepository } from "../infrastructure/mocks/InMemoryMatchRepository"; -import { InMemoryQueueRepository } from "../infrastructure/mocks/InMemoryQueueRepository"; -import { MockGameService } from "../infrastructure/mocks/MockGameService"; -import { MockNotificationService } from "../infrastructure/mocks/MockNotificationService"; -import { createPlayer } from "../domain/models/Player"; -import { createTeam, getAverageRating } from "../domain/models/Team"; -import { MatchState } from "../domain/models/MatchState"; - -const eventBus = new EventBus(); -const mockGameService = new MockGameService(); -const mockNotificationService = new MockNotificationService(); - -const playerRepo = new InMemoryPlayerRepository(); -const teamRepo = new InMemoryTeamRepository(); -const matchRepo = new InMemoryMatchRepository(); -const queueRepo = new InMemoryQueueRepository(); - -const protocolRegistry = new ProtocolRegistry(); -protocolRegistry.register(new StandardOsuProtocol()); -protocolRegistry.register(new HeadToHeadProtocol()); - -const matchService = new MatchService( - matchRepo, - playerRepo, - protocolRegistry, - mockGameService, - mockNotificationService, - eventBus -); - -const queueService = new QueueService(queueRepo, matchService, eventBus, { - minQueueTime: 0, -}); - -eventBus.subscribeAll((event) => { - console.log(`[Event] ${event.type}`); -}); - -type CommandHandler = (...args: string[]) => Promise; - -const commands: Record = { - "player:create": async (osuName?: string, rating?: string) => { - const player = createPlayer({ - discordId: `discord-${Date.now()}`, - osuId: Math.floor(Math.random() * 1000000), - osuName: osuName ?? `Player${Date.now()}`, - rating: rating ? parseInt(rating) : 1000, - }); - await playerRepo.save(player); - console.log(`Created player: ${player.id} (${player.osuName}, rating: ${player.rating})`); - }, - - "player:list": async () => { - const players = await playerRepo.findAll(); - if (players.length === 0) { - console.log("No players found"); - return; - } - console.log("Players:"); - for (const p of players) { - console.log(` ${p.id}: ${p.osuName} (rating: ${p.rating})`); - } - }, - - "team:create": async (name: string, ...playerIds: string[]) => { - if (!name || playerIds.length === 0) { - console.log("Usage: team:create [playerId2] ..."); - return; - } - const players = await Promise.all( - playerIds.map((id) => playerRepo.findById(id)) - ); - const validPlayers = players.filter((p) => p !== null); - if (validPlayers.length === 0) { - console.log("No valid players found"); - return; - } - const team = createTeam({ name, players: validPlayers }); - await teamRepo.save(team); - console.log(`Created team: ${team.id} (${team.name}, avg rating: ${getAverageRating(team)})`); - }, - - "team:list": async () => { - const teams = await teamRepo.findAll(); - if (teams.length === 0) { - console.log("No teams found"); - return; - } - console.log("Teams:"); - for (const t of teams) { - console.log(` ${t.id}: ${t.name} (${t.players.length} players, avg: ${getAverageRating(t)})`); - } - }, - - "queue:join": async (teamId: string, queueType: string = "ranked") => { - if (!teamId) { - console.log("Usage: queue:join [queueType]"); - return; - } - const team = await teamRepo.findById(teamId); - if (!team) { - console.log(`Team ${teamId} not found`); - return; - } - try { - const entry = await queueService.joinQueue(team, queueType); - console.log(`Team ${team.name} joined ${queueType} queue at ${entry.joinedAt.toISOString()}`); - } catch (e) { - console.log(`Error: ${(e as Error).message}`); - } - }, - - "queue:leave": async (teamId: string) => { - if (!teamId) { - console.log("Usage: queue:leave "); - return; - } - try { - await queueService.leaveQueue(teamId); - console.log(`Team ${teamId} left the queue`); - } catch (e) { - console.log(`Error: ${(e as Error).message}`); - } - }, - - "queue:list": async (queueType: string = "ranked") => { - const status = await queueService.getQueueStatus(queueType); - console.log(`Queue (${queueType}): ${status.count} teams`); - for (const e of status.entries) { - console.log(` ${e.teamName} (rating: ${e.rating}, wait: ${Math.round(e.waitTime / 1000)}s)`); - } - }, - - "matchmaking:run": async () => { - const match = await queueService.findMatch("ranked"); - if (match) { - console.log(`Created match ${match.id}: ${match.teams.map((t) => t.name).join(" vs ")}`); - } else { - console.log("No match found (need 2+ teams in queue)"); - } - }, - - "match:status": async (matchId: string) => { - if (!matchId) { - console.log("Usage: match:status "); - return; - } - const match = await matchRepo.findById(matchId); - if (!match) { - console.log(`Match ${matchId} not found`); - return; - } - console.log(`Match: ${match.id}`); - console.log(` State: ${match.state}`); - console.log(` Protocol: ${match.protocol}`); - console.log(` Teams: ${match.teams.map((t) => t.name).join(" vs ")}`); - console.log(` Scores: ${match.teams.map((t) => `${t.name}: ${match.scores.filter((s) => s.winner === t.id).length}`).join(", ")}`); - if (match.winner) { - console.log(` Winner: ${match.winner.name}`); - } - }, - - "match:list": async () => { - const matches = await matchRepo.findAll(); - if (matches.length === 0) { - console.log("No matches found"); - return; - } - console.log("Matches:"); - for (const m of matches) { - console.log(` ${m.id}: ${m.teams.map((t) => t.name).join(" vs ")} [${m.state}]`); - } - }, - - "match:simulate-join": async (matchId: string, osuIdStr: string) => { - if (!matchId || !osuIdStr) { - console.log("Usage: match:simulate-join "); - return; - } - const match = await matchRepo.findById(matchId); - if (!match || !match.lobbyId) { - console.log(`Match ${matchId} not found or has no lobby`); - return; - } - const osuId = parseInt(osuIdStr); - mockGameService.simulatePlayerJoin(match.lobbyId, osuId); - console.log(`Simulated player ${osuId} joining lobby ${match.lobbyId}`); - }, - - "match:simulate-roll": async (matchId: string, osuIdStr: string, rollStr: string) => { - if (!matchId || !osuIdStr || !rollStr) { - console.log("Usage: match:simulate-roll "); - return; - } - const match = await matchRepo.findById(matchId); - if (!match || !match.lobbyId) { - console.log(`Match ${matchId} not found or has no lobby`); - return; - } - const osuId = parseInt(osuIdStr); - const roll = parseInt(rollStr); - mockGameService.simulateRoll(match.lobbyId, osuId, roll); - console.log(`Simulated roll ${roll} from player ${osuId}`); - }, - - "protocol:list": async () => { - const protocols = protocolRegistry.list(); - console.log("Available protocols:"); - for (const p of protocols) { - console.log(` ${p.name}: ${p.description} (${p.teamSize}v${p.teamSize}, best of ${p.bestOf})`); - } - }, - - help: async () => { - console.log("Available commands:"); - console.log(" player:create [osuName] [rating]"); - console.log(" player:list"); - console.log(" team:create [playerId2] ..."); - console.log(" team:list"); - console.log(" queue:join [queueType]"); - console.log(" queue:leave "); - console.log(" queue:list [queueType]"); - console.log(" matchmaking:run"); - console.log(" match:status "); - console.log(" match:list"); - console.log(" match:simulate-join "); - console.log(" match:simulate-roll "); - console.log(" protocol:list"); - console.log(" help"); - }, -}; - -async function main() { - const args = process.argv.slice(2); - const command = args[0]; - const commandArgs = args.slice(1); - - if (!command) { - await commands.help(); - process.exit(0); - } - - const handler = commands[command]; - if (!handler) { - console.log(`Unknown command: ${command}`); - await commands.help(); - process.exit(1); - } - - await handler(...commandArgs); -} - -main().catch(console.error); diff --git a/backend/src/domain/events/DomainEvent.ts b/backend/src/domain/events/DomainEvent.ts deleted file mode 100644 index 7a32728..0000000 --- a/backend/src/domain/events/DomainEvent.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface DomainEvent { - type: string; - timestamp: Date; - payload: T; -} - -export function createEvent(type: string, payload: T): DomainEvent { - return { - type, - timestamp: new Date(), - payload, - }; -} diff --git a/backend/src/domain/events/MatchEvents.ts b/backend/src/domain/events/MatchEvents.ts deleted file mode 100644 index 8ebb232..0000000 --- a/backend/src/domain/events/MatchEvents.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { Match } from "../models/Match"; -import type { MatchState } from "../models/MatchState"; -import type { Player } from "../models/Player"; -import type { Team } from "../models/Team"; -import type { MatchScore } from "../models/Score"; -import type { OsuMap } from "../models/OsuMap"; -import type { DomainEvent } from "./DomainEvent"; -import { createEvent } from "./DomainEvent"; - -export interface MatchCreatedPayload { - match: Match; -} - -export interface MatchStateChangedPayload { - match: Match; - previousState: MatchState; -} - -export interface PlayerJoinedLobbyPayload { - match: Match; - player: Player; -} - -export interface RollCompletedPayload { - match: Match; - player: Player; - roll: number; -} - -export interface MapPickedPayload { - match: Match; - team: Team; - map: OsuMap; -} - -export interface MapBannedPayload { - match: Match; - team: Team; - map: OsuMap; -} - -export interface MapStartedPayload { - match: Match; - map: OsuMap; -} - -export interface MapCompletedPayload { - match: Match; - scores: MatchScore; -} - -export interface MatchCompletedPayload { - match: Match; - winner: Team; -} - -export interface MatchCancelledPayload { - match: Match; - reason: string; -} - -export type MatchCreatedEvent = DomainEvent & { type: "match.created" }; -export type MatchStateChangedEvent = DomainEvent & { type: "match.state_changed" }; -export type PlayerJoinedLobbyEvent = DomainEvent & { type: "match.player_joined" }; -export type RollCompletedEvent = DomainEvent & { type: "match.roll_completed" }; -export type MapPickedEvent = DomainEvent & { type: "match.map_picked" }; -export type MapBannedEvent = DomainEvent & { type: "match.map_banned" }; -export type MapStartedEvent = DomainEvent & { type: "match.map_started" }; -export type MapCompletedEvent = DomainEvent & { type: "match.map_completed" }; -export type MatchCompletedEvent = DomainEvent & { type: "match.completed" }; -export type MatchCancelledEvent = DomainEvent & { type: "match.cancelled" }; - -export type MatchEvent = - | MatchCreatedEvent - | MatchStateChangedEvent - | PlayerJoinedLobbyEvent - | RollCompletedEvent - | MapPickedEvent - | MapBannedEvent - | MapStartedEvent - | MapCompletedEvent - | MatchCompletedEvent - | MatchCancelledEvent; - -export const MatchEvents = { - created: (match: Match): MatchCreatedEvent => - createEvent("match.created", { match }) as MatchCreatedEvent, - - stateChanged: (match: Match, previousState: MatchState): MatchStateChangedEvent => - createEvent("match.state_changed", { match, previousState }) as MatchStateChangedEvent, - - playerJoined: (match: Match, player: Player): PlayerJoinedLobbyEvent => - createEvent("match.player_joined", { match, player }) as PlayerJoinedLobbyEvent, - - rollCompleted: (match: Match, player: Player, roll: number): RollCompletedEvent => - createEvent("match.roll_completed", { match, player, roll }) as RollCompletedEvent, - - mapPicked: (match: Match, team: Team, map: OsuMap): MapPickedEvent => - createEvent("match.map_picked", { match, team, map }) as MapPickedEvent, - - mapBanned: (match: Match, team: Team, map: OsuMap): MapBannedEvent => - createEvent("match.map_banned", { match, team, map }) as MapBannedEvent, - - mapStarted: (match: Match, map: OsuMap): MapStartedEvent => - createEvent("match.map_started", { match, map }) as MapStartedEvent, - - mapCompleted: (match: Match, scores: MatchScore): MapCompletedEvent => - createEvent("match.map_completed", { match, scores }) as MapCompletedEvent, - - completed: (match: Match, winner: Team): MatchCompletedEvent => - createEvent("match.completed", { match, winner }) as MatchCompletedEvent, - - cancelled: (match: Match, reason: string): MatchCancelledEvent => - createEvent("match.cancelled", { match, reason }) as MatchCancelledEvent, -}; diff --git a/backend/src/domain/events/QueueEvents.ts b/backend/src/domain/events/QueueEvents.ts deleted file mode 100644 index 77e0058..0000000 --- a/backend/src/domain/events/QueueEvents.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Team } from "../models/Team"; -import type { QueueEntry } from "../models/QueueEntry"; -import type { DomainEvent } from "./DomainEvent"; -import { createEvent } from "./DomainEvent"; - -export interface TeamQueuedPayload { - entry: QueueEntry; -} - -export interface TeamDequeuedPayload { - teamId: string; - queueType: string; -} - -export interface MatchFoundPayload { - teams: Team[]; - queueType: string; -} - -export type TeamQueuedEvent = DomainEvent & { type: "queue.team_joined" }; -export type TeamDequeuedEvent = DomainEvent & { type: "queue.team_left" }; -export type MatchFoundEvent = DomainEvent & { type: "queue.match_found" }; - -export type QueueEvent = TeamQueuedEvent | TeamDequeuedEvent | MatchFoundEvent; - -export const QueueEvents = { - teamJoined: (entry: QueueEntry): TeamQueuedEvent => - createEvent("queue.team_joined", { entry }) as TeamQueuedEvent, - - teamLeft: (teamId: string, queueType: string): TeamDequeuedEvent => - createEvent("queue.team_left", { teamId, queueType }) as TeamDequeuedEvent, - - matchFound: (teams: Team[], queueType: string): MatchFoundEvent => - createEvent("queue.match_found", { teams, queueType }) as MatchFoundEvent, -}; diff --git a/backend/src/domain/events/index.ts b/backend/src/domain/events/index.ts deleted file mode 100644 index 9d7d966..0000000 --- a/backend/src/domain/events/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./DomainEvent"; -export * from "./MatchEvents"; -export * from "./QueueEvents"; diff --git a/backend/src/domain/interfaces/IGameService.ts b/backend/src/domain/interfaces/IGameService.ts deleted file mode 100644 index 4063bc9..0000000 --- a/backend/src/domain/interfaces/IGameService.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Match } from "../models/Match"; -import type { Player } from "../models/Player"; -import type { Score } from "../models/Score"; - -export interface IGameService { - createLobby(match: Match): Promise; - closeLobby(lobbyId: string): Promise; - invitePlayer(player: Player, lobbyId: string): Promise; - sendMessage(lobbyId: string, message: string): Promise; - setMap(lobbyId: string, mapId: number): Promise; - startMatch(lobbyId: string): Promise; - - onPlayerJoin(callback: (osuId: number, lobbyId: string) => void): void; - onPlayerLeave(callback: (osuId: number, lobbyId: string) => void): void; - onMatchStart(callback: (lobbyId: string) => void): void; - onMatchFinish(callback: (lobbyId: string, scores: Score[]) => void): void; - onRoll(callback: (osuId: number, lobbyId: string, roll: number) => void): void; -} diff --git a/backend/src/domain/interfaces/IMatchProtocol.ts b/backend/src/domain/interfaces/IMatchProtocol.ts deleted file mode 100644 index e69de29..0000000 diff --git a/backend/src/domain/interfaces/IMatchRepository.ts b/backend/src/domain/interfaces/IMatchRepository.ts deleted file mode 100644 index f386c95..0000000 --- a/backend/src/domain/interfaces/IMatchRepository.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Match } from "../models/Match"; -import type { MatchState } from "../models/MatchState"; - -export interface IMatchRepository { - findById(id: string): Promise; - findActive(): Promise; - findByPlayer(playerId: string, limit?: number): Promise; - findByLobbyId(lobbyId: string): Promise; - save(match: Match): Promise; - updateState(matchId: string, state: MatchState): Promise; - findAll(): Promise; -} diff --git a/backend/src/domain/interfaces/INotificationService.ts b/backend/src/domain/interfaces/INotificationService.ts deleted file mode 100644 index a2165f8..0000000 --- a/backend/src/domain/interfaces/INotificationService.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Match } from "../models/Match"; -import type { Team } from "../models/Team"; -import type { MatchScore } from "../models/Score"; - -export interface INotificationService { - announceMatch(match: Match): Promise; - announceMatchResult(match: Match, winner: Team): Promise; - notifyPlayer(playerId: string, message: string): Promise; - notifyTeam(teamId: string, message: string): Promise; - announceMapResult(match: Match, scores: MatchScore): Promise; -} diff --git a/backend/src/domain/interfaces/IPlayerRepository.ts b/backend/src/domain/interfaces/IPlayerRepository.ts deleted file mode 100644 index 1df6117..0000000 --- a/backend/src/domain/interfaces/IPlayerRepository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Player } from "../models/Player"; - -export interface IPlayerRepository { - findById(id: string): Promise; - findByOsuId(osuId: number): Promise; - findByDiscordId(discordId: string): Promise; - save(player: Player): Promise; - updateRating(playerId: string, newRating: number): Promise; - findAll(): Promise; -} diff --git a/backend/src/domain/interfaces/IQueueRepository.ts b/backend/src/domain/interfaces/IQueueRepository.ts deleted file mode 100644 index 8bdb222..0000000 --- a/backend/src/domain/interfaces/IQueueRepository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { QueueEntry } from "../models/QueueEntry"; - -export interface IQueueRepository { - add(entry: QueueEntry): Promise; - remove(teamId: string): Promise; - findByTeamId(teamId: string): Promise; - findByType(queueType: string): Promise; - findClosestByRating( - rating: number, - queueType: string, - exclude?: string[] - ): Promise; - findAll(): Promise; - clear(): Promise; -} diff --git a/backend/src/domain/interfaces/ITeamRepository.ts b/backend/src/domain/interfaces/ITeamRepository.ts deleted file mode 100644 index 516a5d7..0000000 --- a/backend/src/domain/interfaces/ITeamRepository.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Team } from "../models/Team"; - -export interface ITeamRepository { - findById(id: string): Promise; - findByPlayer(playerId: string): Promise; - save(team: Team): Promise; - delete(teamId: string): Promise; - findAll(): Promise; -} diff --git a/backend/src/domain/interfaces/index.ts b/backend/src/domain/interfaces/index.ts deleted file mode 100644 index 4bc264b..0000000 --- a/backend/src/domain/interfaces/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./IPlayerRepository"; -export * from "./ITeamRepository"; -export * from "./IMatchRepository"; -export * from "./IQueueRepository"; -export * from "./IGameService"; -export * from "./INotificationService"; -export * from "./IMatchProtocol"; diff --git a/backend/src/domain/models/Match.ts b/backend/src/domain/models/Match.ts deleted file mode 100644 index dfd3a33..0000000 --- a/backend/src/domain/models/Match.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { Team } from "./Team"; -import type { MatchScore } from "./Score"; -import { MatchState } from "./MatchState"; - -export interface Match { - id: string; - teams: Team[]; - protocol: string; - state: MatchState; - scores: MatchScore[]; - winner?: Team; - lobbyId?: string; - createdAt: Date; - startedAt?: Date; - endedAt?: Date; - - rolls: Map; - bans: Map; - picks: number[]; - currentPicker?: string; - currentBanner?: string; - joinedPlayers: Set; -} - -export function createMatch(params: { - id?: string; - teams: Team[]; - protocol: string; - state?: MatchState; - lobbyId?: string; - createdAt?: Date; -}): Match { - if (params.teams.length < 2) { - throw new Error("Match must have at least two teams"); - } - - return { - id: params.id ?? crypto.randomUUID(), - teams: params.teams, - protocol: params.protocol, - state: params.state ?? MatchState.CREATED, - scores: [], - lobbyId: params.lobbyId, - createdAt: params.createdAt ?? new Date(), - rolls: new Map(), - bans: new Map(), - picks: [], - joinedPlayers: new Set(), - }; -} - -export function getTeamWins(match: Match, teamId: string): number { - return match.scores.filter((s) => s.winner === teamId).length; -} - -export function getMatchWinner(match: Match, bestOf: number): Team | null { - const winsNeeded = Math.ceil(bestOf / 2); - - for (const team of match.teams) { - if (getTeamWins(match, team.id) >= winsNeeded) { - return team; - } - } - - return null; -} - -export function allPlayersJoined(match: Match): boolean { - const allPlayerIds = match.teams.flatMap((t) => t.players.map((p) => p.id)); - return allPlayerIds.every((id) => match.joinedPlayers.has(id)); -} diff --git a/backend/src/domain/models/MatchState.ts b/backend/src/domain/models/MatchState.ts deleted file mode 100644 index 7419610..0000000 --- a/backend/src/domain/models/MatchState.ts +++ /dev/null @@ -1,20 +0,0 @@ -export enum MatchState { - CREATED = "created", - LOBBY_CREATED = "lobby_created", - WAITING_PLAYERS = "waiting_players", - ROLLING = "rolling", - PICKING = "picking", - BANNING = "banning", - PLAYING = "playing", - MAP_COMPLETE = "map_complete", - COMPLETED = "completed", - CANCELLED = "cancelled", -} - -export function isTerminalState(state: MatchState): boolean { - return state === MatchState.COMPLETED || state === MatchState.CANCELLED; -} - -export function isActiveState(state: MatchState): boolean { - return !isTerminalState(state); -} diff --git a/backend/src/domain/models/OsuMap.ts b/backend/src/domain/models/OsuMap.ts deleted file mode 100644 index b7c6354..0000000 --- a/backend/src/domain/models/OsuMap.ts +++ /dev/null @@ -1,45 +0,0 @@ -export interface OsuMap { - id: number; - beatmapSetId: number; - title: string; - artist: string; - difficulty: string; - starRating: number; - bpm: number; - length: number; - mod?: string; -} - -export interface Mappool { - id: string; - name: string; - maps: OsuMap[]; -} - -export function createOsuMap(params: { - id: number; - beatmapSetId?: number; - title: string; - artist: string; - difficulty: string; - starRating?: number; - bpm?: number; - length?: number; - mod?: string; -}): OsuMap { - return { - id: params.id, - beatmapSetId: params.beatmapSetId ?? 0, - title: params.title, - artist: params.artist, - difficulty: params.difficulty, - starRating: params.starRating ?? 0, - bpm: params.bpm ?? 0, - length: params.length ?? 0, - mod: params.mod, - }; -} - -export function getMapsByMod(mappool: Mappool, mod: string): OsuMap[] { - return mappool.maps.filter((m) => m.mod === mod); -} diff --git a/backend/src/domain/models/Player.ts b/backend/src/domain/models/Player.ts deleted file mode 100644 index 078224d..0000000 --- a/backend/src/domain/models/Player.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface Player { - id: string; - discordId: string; - osuId: number; - osuName: string; - displayName: string; - rating: number; - createdAt: Date; -} - -export function createPlayer(params: { - id?: string; - discordId: string; - osuId: number; - osuName: string; - displayName?: string; - rating?: number; - createdAt?: Date; -}): Player { - return { - id: params.id ?? crypto.randomUUID(), - discordId: params.discordId, - osuId: params.osuId, - osuName: params.osuName, - displayName: params.displayName ?? params.osuName, - rating: params.rating ?? 1000, - createdAt: params.createdAt ?? new Date(), - }; -} diff --git a/backend/src/domain/models/QueueEntry.ts b/backend/src/domain/models/QueueEntry.ts deleted file mode 100644 index 0bd159f..0000000 --- a/backend/src/domain/models/QueueEntry.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Team } from "./Team"; - -export interface QueueEntry { - id: string; - team: Team; - queueType: string; - joinedAt: Date; -} - -export function createQueueEntry(params: { - id?: string; - team: Team; - queueType: string; - joinedAt?: Date; -}): QueueEntry { - return { - id: params.id ?? crypto.randomUUID(), - team: params.team, - queueType: params.queueType, - joinedAt: params.joinedAt ?? new Date(), - }; -} diff --git a/backend/src/domain/models/Score.ts b/backend/src/domain/models/Score.ts deleted file mode 100644 index 16e4c15..0000000 --- a/backend/src/domain/models/Score.ts +++ /dev/null @@ -1,38 +0,0 @@ -export interface Score { - playerId: string; - score: number; - accuracy: number; - maxCombo: number; - misses: number; - mods?: string[]; -} - -export interface MatchScore { - id: string; - mapId: number; - teamScores: Map; - playerScores: Map; - winner: string; -} - -export function createMatchScore(params: { - id?: string; - mapId: number; - teamScores: Map; - playerScores: Map; - winner: string; -}): MatchScore { - return { - id: params.id ?? crypto.randomUUID(), - mapId: params.mapId, - teamScores: params.teamScores, - playerScores: params.playerScores, - winner: params.winner, - }; -} - -export function calculateTeamScore(scores: Score[], teamPlayerIds: string[]): number { - return scores - .filter((s) => teamPlayerIds.includes(s.playerId)) - .reduce((sum, s) => sum + s.score, 0); -} diff --git a/backend/src/domain/models/Team.ts b/backend/src/domain/models/Team.ts deleted file mode 100644 index 6e8bc39..0000000 --- a/backend/src/domain/models/Team.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Player } from "./Player"; - -export interface Team { - id: string; - name: string; - players: Player[]; - captain: Player; - createdAt: Date; -} - -export function createTeam(params: { - id?: string; - name: string; - players: Player[]; - captain?: Player; - createdAt?: Date; -}): Team { - if (params.players.length === 0) { - throw new Error("Team must have at least one player"); - } - - return { - id: params.id ?? crypto.randomUUID(), - name: params.name, - players: params.players, - captain: params.captain ?? params.players[0], - createdAt: params.createdAt ?? new Date(), - }; -} - -export function getAverageRating(team: Team): number { - if (team.players.length === 0) return 0; - const total = team.players.reduce((sum, p) => sum + p.rating, 0); - return Math.round(total / team.players.length); -} diff --git a/backend/src/domain/models/index.ts b/backend/src/domain/models/index.ts deleted file mode 100644 index 4263f9e..0000000 --- a/backend/src/domain/models/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./Player"; -export * from "./Team"; -export * from "./Match"; -export * from "./MatchState"; -export * from "./Score"; -export * from "./QueueEntry"; -export * from "./OsuMap"; diff --git a/backend/src/domain/protocols/HeadToHeadProtocol.ts b/backend/src/domain/protocols/HeadToHeadProtocol.ts deleted file mode 100644 index 7a67378..0000000 --- a/backend/src/domain/protocols/HeadToHeadProtocol.ts +++ /dev/null @@ -1,160 +0,0 @@ -import type { - IMatchProtocol, - ProtocolContext, - RollResult, -} from "./IMatchProtocol"; -import type { Match } from "../models/Match"; -import { MatchState } from "../models/MatchState"; -import type { Team } from "../models/Team"; -import type { OsuMap } from "../models/OsuMap"; -import type { Score } from "../models/Score"; -import { getTeamWins } from "../models/Match"; - -export class HeadToHeadProtocol implements IMatchProtocol { - readonly name = "head-to-head"; - readonly description = "1v1 head-to-head format"; - readonly teamSize = 1; - readonly bestOf = 5; - readonly hasPickBan = false; - readonly hasFreemod = false; - - async onMatchStart( - match: Match, - context: ProtocolContext - ): Promise { - const team1 = match.teams[0]; - const team2 = match.teams[1]; - await context.sendMessage(`1v1 Match: ${team1.name} vs ${team2.name}`); - await context.sendMessage("Waiting for players to join..."); - return MatchState.WAITING_PLAYERS; - } - - async onAllPlayersJoined( - _match: Match, - context: ProtocolContext - ): Promise { - await context.sendMessage("Both players have joined! Please roll (!roll)"); - return MatchState.ROLLING; - } - - async onRollComplete( - match: Match, - rolls: RollResult[], - context: ProtocolContext - ): Promise { - const team1Roll = rolls.find((r) => r.teamId === match.teams[0].id); - const team2Roll = rolls.find((r) => r.teamId === match.teams[1].id); - - if (!team1Roll || !team2Roll) { - return null; - } - - const winningTeam = - team1Roll.roll > team2Roll.roll ? match.teams[0] : match.teams[1]; - - match.currentPicker = winningTeam.id; - - await context.sendMessage( - `${winningTeam.name} wins the roll and picks first!` - ); - - return MatchState.PICKING; - } - - async onBan( - _match: Match, - _team: Team, - _map: OsuMap, - _context: ProtocolContext - ): Promise { - return null; - } - - async onPick( - match: Match, - team: Team, - map: OsuMap, - context: ProtocolContext - ): Promise { - match.picks.push(map.id); - await context.sendMessage(`${team.name} picked ${map.title} [${map.difficulty}]`); - await context.setMap(map.id); - - return MatchState.PLAYING; - } - - async onMapComplete( - match: Match, - _scores: Score[], - context: ProtocolContext - ): Promise { - const winner = this.calculateMatchWinner(match); - if (winner) { - return MatchState.COMPLETED; - } - - const currentPicker = match.currentPicker; - match.currentPicker = - currentPicker === match.teams[0].id - ? match.teams[1].id - : match.teams[0].id; - - const team1 = match.teams[0]; - const team2 = match.teams[1]; - await context.sendMessage( - `Score: ${team1.name} ${getTeamWins(match, team1.id)} - ${getTeamWins(match, team2.id)} ${team2.name}` - ); - - return MatchState.PICKING; - } - - isValidPick(match: Match, team: Team, map: OsuMap): boolean { - if (match.currentPicker !== team.id) return false; - if (match.picks.includes(map.id)) return false; - return true; - } - - isValidBan(_match: Match, _team: Team, _map: OsuMap): boolean { - return false; - } - - calculateMapWinner(match: Match, scores: Score[]): string { - const team1 = match.teams[0]; - const team2 = match.teams[1]; - - const team1Score = scores.find((s) => - team1.players.some((p) => p.id === s.playerId) - )?.score ?? 0; - - const team2Score = scores.find((s) => - team2.players.some((p) => p.id === s.playerId) - )?.score ?? 0; - - return team1Score > team2Score ? team1.id : team2.id; - } - - calculateMatchWinner(match: Match): string | null { - const winsNeeded = this.getWinsNeeded(); - - for (const team of match.teams) { - if (getTeamWins(match, team.id) >= winsNeeded) { - return team.id; - } - } - - return null; - } - - getPickingTeam(match: Match): Team { - const team = match.teams.find((t) => t.id === match.currentPicker); - return team ?? match.teams[0]; - } - - getBanningTeam(match: Match): Team { - return match.teams[0]; - } - - getWinsNeeded(): number { - return Math.ceil(this.bestOf / 2); - } -} diff --git a/backend/src/domain/protocols/IMatchProtocol.ts b/backend/src/domain/protocols/IMatchProtocol.ts deleted file mode 100644 index 8a8551e..0000000 --- a/backend/src/domain/protocols/IMatchProtocol.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { Match } from "../models/Match"; -import type { MatchState } from "../models/MatchState"; -import type { Team } from "../models/Team"; -import type { OsuMap } from "../models/OsuMap"; -import type { Score } from "../models/Score"; - -export interface RollResult { - teamId: string; - playerId: string; - roll: number; -} - -export interface ProtocolContext { - sendMessage: (message: string) => Promise; - setMap: (mapId: number) => Promise; - startGame: () => Promise; -} - -export interface IMatchProtocol { - readonly name: string; - readonly description: string; - - readonly teamSize: number; - readonly bestOf: number; - readonly hasPickBan: boolean; - readonly hasFreemod: boolean; - - onMatchStart(match: Match, context: ProtocolContext): Promise; - onAllPlayersJoined(match: Match, context: ProtocolContext): Promise; - onRollComplete( - match: Match, - rolls: RollResult[], - context: ProtocolContext - ): Promise; - onPick( - match: Match, - team: Team, - map: OsuMap, - context: ProtocolContext - ): Promise; - onBan( - match: Match, - team: Team, - map: OsuMap, - context: ProtocolContext - ): Promise; - onMapComplete( - match: Match, - scores: Score[], - context: ProtocolContext - ): Promise; - - isValidPick(match: Match, team: Team, map: OsuMap): boolean; - isValidBan(match: Match, team: Team, map: OsuMap): boolean; - - calculateMapWinner(match: Match, scores: Score[]): string; - calculateMatchWinner(match: Match): string | null; - - getPickingTeam(match: Match): Team; - getBanningTeam(match: Match): Team; - - getWinsNeeded(): number; -} diff --git a/backend/src/domain/protocols/ProtocolRegistry.ts b/backend/src/domain/protocols/ProtocolRegistry.ts deleted file mode 100644 index e54aeef..0000000 --- a/backend/src/domain/protocols/ProtocolRegistry.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { IMatchProtocol } from "./IMatchProtocol"; - -export class ProtocolRegistry { - private protocols: Map = new Map(); - - register(protocol: IMatchProtocol): void { - if (this.protocols.has(protocol.name)) { - throw new Error(`Protocol "${protocol.name}" is already registered`); - } - this.protocols.set(protocol.name, protocol); - } - - get(name: string): IMatchProtocol { - const protocol = this.protocols.get(name); - if (!protocol) { - throw new Error(`Protocol "${name}" not found`); - } - return protocol; - } - - has(name: string): boolean { - return this.protocols.has(name); - } - - list(): IMatchProtocol[] { - return Array.from(this.protocols.values()); - } - - names(): string[] { - return Array.from(this.protocols.keys()); - } -} diff --git a/backend/src/domain/protocols/StandardOsuProtocol.ts b/backend/src/domain/protocols/StandardOsuProtocol.ts deleted file mode 100644 index 36fa643..0000000 --- a/backend/src/domain/protocols/StandardOsuProtocol.ts +++ /dev/null @@ -1,192 +0,0 @@ -import type { - IMatchProtocol, - ProtocolContext, - RollResult, -} from "./IMatchProtocol"; -import type { Match } from "../models/Match"; -import { MatchState } from "../models/MatchState"; -import type { Team } from "../models/Team"; -import type { OsuMap } from "../models/OsuMap"; -import type { Score } from "../models/Score"; -import { getTeamWins } from "../models/Match"; - -export class StandardOsuProtocol implements IMatchProtocol { - readonly name = "standard-osu"; - readonly description = "Standard 4v4 tournament format with pick/ban"; - readonly teamSize = 4; - readonly bestOf = 9; - readonly hasPickBan = true; - readonly hasFreemod = true; - - async onMatchStart( - _match: Match, - context: ProtocolContext - ): Promise { - await context.sendMessage("Welcome to the match!"); - await context.sendMessage("Waiting for all players to join..."); - return MatchState.WAITING_PLAYERS; - } - - async onAllPlayersJoined( - _match: Match, - context: ProtocolContext - ): Promise { - await context.sendMessage("All players have joined! Please roll (!roll)"); - return MatchState.ROLLING; - } - - async onRollComplete( - match: Match, - rolls: RollResult[], - context: ProtocolContext - ): Promise { - const team1Roll = rolls.find((r) => r.teamId === match.teams[0].id); - const team2Roll = rolls.find((r) => r.teamId === match.teams[1].id); - - if (!team1Roll || !team2Roll) { - return null; - } - - const winningTeam = - team1Roll.roll > team2Roll.roll ? match.teams[0] : match.teams[1]; - const losingTeam = - team1Roll.roll > team2Roll.roll ? match.teams[1] : match.teams[0]; - - match.currentBanner = winningTeam.id; - match.currentPicker = losingTeam.id; - - await context.sendMessage( - `${winningTeam.name} wins the roll! They will ban first, ${losingTeam.name} picks first.` - ); - - return MatchState.BANNING; - } - - async onBan( - match: Match, - team: Team, - map: OsuMap, - context: ProtocolContext - ): Promise { - const existingBans = match.bans.get(team.id); - const bans = existingBans ? [...existingBans] : []; - bans.push(map.id); - match.bans.set(team.id, bans); - - await context.sendMessage(`${team.name} banned ${map.title} [${map.difficulty}]`); - - const totalBans = Array.from(match.bans.values()).flat().length; - if (totalBans >= 2) { - return MatchState.PICKING; - } - - const currentBanner = match.currentBanner; - match.currentBanner = - currentBanner === match.teams[0].id - ? match.teams[1].id - : match.teams[0].id; - - return null; - } - - async onPick( - match: Match, - team: Team, - map: OsuMap, - context: ProtocolContext - ): Promise { - match.picks.push(map.id); - await context.sendMessage(`${team.name} picked ${map.title} [${map.difficulty}]`); - await context.setMap(map.id); - - return MatchState.PLAYING; - } - - async onMapComplete( - match: Match, - _scores: Score[], - context: ProtocolContext - ): Promise { - const winner = this.calculateMatchWinner(match); - if (winner) { - return MatchState.COMPLETED; - } - - const currentPicker = match.currentPicker; - match.currentPicker = - currentPicker === match.teams[0].id - ? match.teams[1].id - : match.teams[0].id; - - const team1 = match.teams[0]; - const team2 = match.teams[1]; - await context.sendMessage( - `Score: ${team1.name} ${getTeamWins(match, team1.id)} - ${getTeamWins(match, team2.id)} ${team2.name}` - ); - - return MatchState.PICKING; - } - - isValidPick(match: Match, team: Team, map: OsuMap): boolean { - if (match.currentPicker !== team.id) return false; - - const allBans = Array.from(match.bans.values()).flat(); - if (allBans.includes(map.id)) return false; - - if (match.picks.includes(map.id)) return false; - - return true; - } - - isValidBan(match: Match, team: Team, map: OsuMap): boolean { - if (match.currentBanner !== team.id) return false; - - const allBans = Array.from(match.bans.values()).flat(); - if (allBans.includes(map.id)) return false; - - return true; - } - - calculateMapWinner(match: Match, scores: Score[]): string { - const team1 = match.teams[0]; - const team2 = match.teams[1]; - const team1PlayerIds = team1.players.map((p) => p.id); - const team2PlayerIds = team2.players.map((p) => p.id); - - const team1Score = scores - .filter((s) => team1PlayerIds.includes(s.playerId)) - .reduce((sum, s) => sum + s.score, 0); - - const team2Score = scores - .filter((s) => team2PlayerIds.includes(s.playerId)) - .reduce((sum, s) => sum + s.score, 0); - - return team1Score > team2Score ? team1.id : team2.id; - } - - calculateMatchWinner(match: Match): string | null { - const winsNeeded = this.getWinsNeeded(); - - for (const team of match.teams) { - if (getTeamWins(match, team.id) >= winsNeeded) { - return team.id; - } - } - - return null; - } - - getPickingTeam(match: Match): Team { - const team = match.teams.find((t) => t.id === match.currentPicker); - return team ?? match.teams[0]; - } - - getBanningTeam(match: Match): Team { - const team = match.teams.find((t) => t.id === match.currentBanner); - return team ?? match.teams[0]; - } - - getWinsNeeded(): number { - return Math.ceil(this.bestOf / 2); - } -} diff --git a/backend/src/domain/protocols/index.ts b/backend/src/domain/protocols/index.ts deleted file mode 100644 index 5b304d3..0000000 --- a/backend/src/domain/protocols/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./IMatchProtocol"; -export * from "./StandardOsuProtocol"; -export * from "./HeadToHeadProtocol"; -export * from "./ProtocolRegistry"; diff --git a/backend/src/index.ts b/backend/src/index.ts deleted file mode 100644 index 04a9174..0000000 --- a/backend/src/index.ts +++ /dev/null @@ -1,197 +0,0 @@ -import type { ServerWebSocket } from "bun"; -import { EventBus } from "./application/events/EventBus"; -import { MatchService } from "./application/services/MatchService"; -import { QueueService } from "./application/services/QueueService"; -import { ProtocolRegistry } from "./domain/protocols/ProtocolRegistry"; -import { StandardOsuProtocol } from "./domain/protocols/StandardOsuProtocol"; -import { HeadToHeadProtocol } from "./domain/protocols/HeadToHeadProtocol"; -import { InMemoryPlayerRepository } from "./infrastructure/mocks/InMemoryPlayerRepository"; -import { InMemoryTeamRepository } from "./infrastructure/mocks/InMemoryTeamRepository"; -import { InMemoryMatchRepository } from "./infrastructure/mocks/InMemoryMatchRepository"; -import { InMemoryQueueRepository } from "./infrastructure/mocks/InMemoryQueueRepository"; -import { MockGameService } from "./infrastructure/mocks/MockGameService"; -import { MockNotificationService } from "./infrastructure/mocks/MockNotificationService"; -import { WebSocketService } from "./infrastructure/websocket/WebSocketService"; -import { getTeamWins } from "./domain/models/Match"; -import { getAverageRating } from "./domain/models/Team"; - -interface WebSocketData { - id: string; - subscribedMatches: Set; -} - -const eventBus = new EventBus(); -const mockGameService = new MockGameService(); -const mockNotificationService = new MockNotificationService(); - -const playerRepo = new InMemoryPlayerRepository(); -const teamRepo = new InMemoryTeamRepository(); -const matchRepo = new InMemoryMatchRepository(); -const queueRepo = new InMemoryQueueRepository(); - -const protocolRegistry = new ProtocolRegistry(); -protocolRegistry.register(new StandardOsuProtocol()); -protocolRegistry.register(new HeadToHeadProtocol()); - -const matchService = new MatchService( - matchRepo, - playerRepo, - protocolRegistry, - mockGameService, - mockNotificationService, - eventBus -); - -const queueService = new QueueService(queueRepo, matchService, eventBus, { - minQueueTime: 0, - protocol: "head-to-head", -}); - -const wsService = new WebSocketService(eventBus); - -const server = Bun.serve({ - port: process.env.PORT ? parseInt(process.env.PORT) : 3000, - - async fetch(req, server) { - const url = new URL(req.url); - - if (url.pathname === "/ws") { - const upgraded = server.upgrade(req, { - data: { - id: crypto.randomUUID(), - subscribedMatches: new Set(), - }, - }); - if (upgraded) return undefined; - return new Response("WebSocket upgrade failed", { status: 400 }); - } - - if (url.pathname === "/api/health") { - return Response.json({ status: "ok", timestamp: new Date().toISOString() }); - } - - if (url.pathname === "/api/queue/status" && req.method === "GET") { - const queueType = url.searchParams.get("type") ?? "ranked"; - const status = await queueService.getQueueStatus(queueType); - return Response.json(status); - } - - if (url.pathname === "/api/queue/join" && req.method === "POST") { - try { - const body = await req.json(); - const team = await teamRepo.findById(body.teamId); - if (!team) { - return Response.json({ error: "Team not found" }, { status: 404 }); - } - const entry = await queueService.joinQueue(team, body.queueType ?? "ranked"); - return Response.json({ success: true, entry: { id: entry.id, joinedAt: entry.joinedAt } }); - } catch (error) { - return Response.json({ error: (error as Error).message }, { status: 400 }); - } - } - - if (url.pathname === "/api/queue/leave" && req.method === "POST") { - try { - const body = await req.json(); - await queueService.leaveQueue(body.teamId); - return Response.json({ success: true }); - } catch (error) { - return Response.json({ error: (error as Error).message }, { status: 400 }); - } - } - - if (url.pathname.startsWith("/api/matches/") && req.method === "GET") { - const matchId = url.pathname.split("/").pop(); - if (!matchId) { - return Response.json({ error: "Match ID required" }, { status: 400 }); - } - const match = await matchRepo.findById(matchId); - if (!match) { - return Response.json({ error: "Match not found" }, { status: 404 }); - } - return Response.json({ - id: match.id, - state: match.state, - protocol: match.protocol, - teams: match.teams.map((t) => ({ - id: t.id, - name: t.name, - players: t.players.map((p) => ({ - id: p.id, - displayName: p.displayName, - rating: p.rating, - })), - averageRating: getAverageRating(t), - wins: getTeamWins(match, t.id), - })), - winner: match.winner ? { id: match.winner.id, name: match.winner.name } : null, - createdAt: match.createdAt, - startedAt: match.startedAt, - endedAt: match.endedAt, - }); - } - - if (url.pathname === "/api/matches" && req.method === "GET") { - const matches = await matchRepo.findAll(); - return Response.json( - matches.map((m) => ({ - id: m.id, - state: m.state, - teams: m.teams.map((t) => t.name), - winner: m.winner?.name ?? null, - createdAt: m.createdAt, - })) - ); - } - - if (url.pathname === "/api/players" && req.method === "GET") { - const players = await playerRepo.findAll(); - return Response.json(players); - } - - if (url.pathname === "/api/teams" && req.method === "GET") { - const teams = await teamRepo.findAll(); - return Response.json( - teams.map((t) => ({ - id: t.id, - name: t.name, - playerCount: t.players.length, - averageRating: getAverageRating(t), - })) - ); - } - - if (url.pathname === "/api/protocols" && req.method === "GET") { - const protocols = protocolRegistry.list(); - return Response.json( - protocols.map((p) => ({ - name: p.name, - description: p.description, - teamSize: p.teamSize, - bestOf: p.bestOf, - hasPickBan: p.hasPickBan, - })) - ); - } - - return new Response("Not Found", { status: 404 }); - }, - - websocket: { - open(ws: ServerWebSocket) { - wsService.handleOpen(ws); - }, - message(ws: ServerWebSocket, message: string | Buffer) { - wsService.handleMessage(ws, message); - }, - close(ws: ServerWebSocket) { - wsService.handleClose(ws); - }, - }, -}); - -console.log(`Server running at http://localhost:${server.port}`); -console.log(`WebSocket available at ws://localhost:${server.port}/ws`); - -queueService.startAutoMatchmaking(5000); -console.log("Auto-matchmaking started (5s interval)"); diff --git a/backend/src/infrastructure/database/PostgresMatchRepository.ts b/backend/src/infrastructure/database/PostgresMatchRepository.ts deleted file mode 100644 index 27a0efd..0000000 --- a/backend/src/infrastructure/database/PostgresMatchRepository.ts +++ /dev/null @@ -1,293 +0,0 @@ -import type { SQL } from "bun"; -import type { IMatchRepository } from "../../domain/interfaces/IMatchRepository"; -import type { Match } from "../../domain/models/Match"; -import { MatchState, isActiveState } from "../../domain/models/MatchState"; -import type { Team } from "../../domain/models/Team"; -import type { Player } from "../../domain/models/Player"; -import type { MatchScore, Score } from "../../domain/models/Score"; - -interface MatchRow { - id: string; - protocol: string; - state: string; - lobby_id: string | null; - winner_team_id: string | null; - created_at: Date; - started_at: Date | null; - ended_at: Date | null; -} - -interface PlayerRow { - id: string; - discord_id: string; - osu_id: number; - osu_name: string; - display_name: string; - rating: number; - created_at: Date; -} - -interface TeamRow { - id: string; - name: string; - captain_id: string; - created_at: Date; -} - -export class PostgresMatchRepository implements IMatchRepository { - constructor(private db: SQL) {} - - private rowToPlayer(row: PlayerRow): Player { - return { - id: row.id, - discordId: row.discord_id, - osuId: row.osu_id, - osuName: row.osu_name, - displayName: row.display_name, - rating: row.rating, - createdAt: row.created_at, - }; - } - - private async loadTeamPlayers(teamId: string): Promise { - const rows = await this.db` - SELECT p.* FROM players p - JOIN team_players tp ON tp.player_id = p.id - WHERE tp.team_id = ${teamId} - `; - return rows.map((row) => this.rowToPlayer(row as PlayerRow)); - } - - private async loadMatchTeams(matchId: string): Promise { - const teamRows = await this.db` - SELECT t.*, mt.team_order FROM teams t - JOIN match_teams mt ON mt.team_id = t.id - WHERE mt.match_id = ${matchId} - ORDER BY mt.team_order - `; - - const teams: Team[] = []; - for (const row of teamRows as (TeamRow & { team_order: number })[]) { - const players = await this.loadTeamPlayers(row.id); - const captain = players.find((p) => p.id === row.captain_id) ?? players[0]; - teams.push({ - id: row.id, - name: row.name, - players, - captain, - createdAt: row.created_at, - }); - } - - return teams; - } - - private async loadMatchScores(matchId: string): Promise { - const scoreRows = await this.db` - SELECT * FROM match_scores WHERE match_id = ${matchId} ORDER BY map_order - `; - - const scores: MatchScore[] = []; - for (const row of scoreRows as any[]) { - const playerScoreRows = await this.db` - SELECT * FROM player_scores WHERE match_score_id = ${row.id} - `; - - const teamScores = new Map(); - const playerScores = new Map(); - - for (const ps of playerScoreRows as any[]) { - playerScores.set(ps.player_id, { - playerId: ps.player_id, - score: ps.score, - accuracy: ps.accuracy, - maxCombo: ps.max_combo, - misses: ps.misses, - mods: ps.mods, - }); - } - - scores.push({ - id: row.id, - mapId: row.map_id, - teamScores, - playerScores, - winner: row.winner_team_id, - }); - } - - return scores; - } - - private async loadMatchRolls(matchId: string): Promise> { - const rows = await this.db`SELECT * FROM match_rolls WHERE match_id = ${matchId}`; - const rolls = new Map(); - for (const row of rows as any[]) { - rolls.set(row.team_id, row.roll_value); - } - return rolls; - } - - private async loadMatchBans(matchId: string): Promise> { - const rows = await this.db` - SELECT * FROM match_bans WHERE match_id = ${matchId} ORDER BY ban_order - `; - const bans = new Map(); - for (const row of rows as any[]) { - const teamBans = bans.get(row.team_id) ?? []; - teamBans.push(row.map_id); - bans.set(row.team_id, teamBans); - } - return bans; - } - - private async loadMatchPicks(matchId: string): Promise { - const rows = await this.db` - SELECT * FROM match_picks WHERE match_id = ${matchId} ORDER BY pick_order - `; - return (rows as any[]).map((r) => r.map_id); - } - - async findById(id: string): Promise { - const rows = await this.db`SELECT * FROM matches WHERE id = ${id}`; - if (rows.length === 0) return null; - - const row = rows[0] as MatchRow; - const teams = await this.loadMatchTeams(id); - const scores = await this.loadMatchScores(id); - const rolls = await this.loadMatchRolls(id); - const bans = await this.loadMatchBans(id); - const picks = await this.loadMatchPicks(id); - - const winner = row.winner_team_id - ? teams.find((t) => t.id === row.winner_team_id) - : undefined; - - return { - id: row.id, - teams, - protocol: row.protocol, - state: row.state as MatchState, - scores, - winner, - lobbyId: row.lobby_id ?? undefined, - createdAt: row.created_at, - startedAt: row.started_at ?? undefined, - endedAt: row.ended_at ?? undefined, - rolls, - bans, - picks, - joinedPlayers: new Set(), - currentPicker: undefined, - currentBanner: undefined, - }; - } - - async findActive(): Promise { - const rows = await this.db` - SELECT id FROM matches WHERE state NOT IN ('completed', 'cancelled') - `; - const matches: Match[] = []; - for (const row of rows as { id: string }[]) { - const match = await this.findById(row.id); - if (match) matches.push(match); - } - return matches; - } - - async findByPlayer(playerId: string, limit?: number): Promise { - const query = limit - ? this.db` - SELECT DISTINCT m.id FROM matches m - JOIN match_teams mt ON mt.match_id = m.id - JOIN team_players tp ON tp.team_id = mt.team_id - WHERE tp.player_id = ${playerId} - ORDER BY m.created_at DESC - LIMIT ${limit} - ` - : this.db` - SELECT DISTINCT m.id FROM matches m - JOIN match_teams mt ON mt.match_id = m.id - JOIN team_players tp ON tp.team_id = mt.team_id - WHERE tp.player_id = ${playerId} - ORDER BY m.created_at DESC - `; - - const rows = await query; - const matches: Match[] = []; - for (const row of rows as { id: string }[]) { - const match = await this.findById(row.id); - if (match) matches.push(match); - } - return matches; - } - - async findByLobbyId(lobbyId: string): Promise { - const rows = await this.db`SELECT id FROM matches WHERE lobby_id = ${lobbyId}`; - if (rows.length === 0) return null; - return this.findById((rows[0] as { id: string }).id); - } - - async save(match: Match): Promise { - await this.db` - INSERT INTO matches (id, protocol, state, lobby_id, winner_team_id, created_at, started_at, ended_at) - VALUES (${match.id}, ${match.protocol}, ${match.state}, ${match.lobbyId ?? null}, ${match.winner?.id ?? null}, ${match.createdAt}, ${match.startedAt ?? null}, ${match.endedAt ?? null}) - ON CONFLICT (id) DO UPDATE SET - protocol = EXCLUDED.protocol, - state = EXCLUDED.state, - lobby_id = EXCLUDED.lobby_id, - winner_team_id = EXCLUDED.winner_team_id, - started_at = EXCLUDED.started_at, - ended_at = EXCLUDED.ended_at - `; - - await this.db`DELETE FROM match_teams WHERE match_id = ${match.id}`; - for (let i = 0; i < match.teams.length; i++) { - await this.db` - INSERT INTO match_teams (match_id, team_id, team_order) - VALUES (${match.id}, ${match.teams[i].id}, ${i}) - `; - } - - await this.db`DELETE FROM match_rolls WHERE match_id = ${match.id}`; - for (const [teamId, roll] of match.rolls) { - await this.db` - INSERT INTO match_rolls (match_id, team_id, roll_value) - VALUES (${match.id}, ${teamId}, ${roll}) - `; - } - - await this.db`DELETE FROM match_bans WHERE match_id = ${match.id}`; - let banOrder = 0; - for (const [teamId, mapIds] of match.bans) { - for (const mapId of mapIds) { - await this.db` - INSERT INTO match_bans (match_id, team_id, map_id, ban_order) - VALUES (${match.id}, ${teamId}, ${mapId}, ${banOrder++}) - `; - } - } - - await this.db`DELETE FROM match_picks WHERE match_id = ${match.id}`; - for (let i = 0; i < match.picks.length; i++) { - await this.db` - INSERT INTO match_picks (match_id, map_id, pick_order) - VALUES (${match.id}, ${match.picks[i]}, ${i}) - `; - } - } - - async updateState(matchId: string, state: MatchState): Promise { - await this.db`UPDATE matches SET state = ${state} WHERE id = ${matchId}`; - } - - async findAll(): Promise { - const rows = await this.db`SELECT id FROM matches ORDER BY created_at DESC`; - const matches: Match[] = []; - for (const row of rows as { id: string }[]) { - const match = await this.findById(row.id); - if (match) matches.push(match); - } - return matches; - } -} diff --git a/backend/src/infrastructure/database/PostgresPlayerRepository.ts b/backend/src/infrastructure/database/PostgresPlayerRepository.ts deleted file mode 100644 index e9122c4..0000000 --- a/backend/src/infrastructure/database/PostgresPlayerRepository.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { SQL } from "bun"; -import type { IPlayerRepository } from "../../domain/interfaces/IPlayerRepository"; -import type { Player } from "../../domain/models/Player"; - -interface PlayerRow { - id: string; - discord_id: string; - osu_id: number; - osu_name: string; - display_name: string; - rating: number; - created_at: Date; -} - -export class PostgresPlayerRepository implements IPlayerRepository { - constructor(private db: SQL) {} - - private rowToPlayer(row: PlayerRow): Player { - return { - id: row.id, - discordId: row.discord_id, - osuId: row.osu_id, - osuName: row.osu_name, - displayName: row.display_name, - rating: row.rating, - createdAt: row.created_at, - }; - } - - async findById(id: string): Promise { - const rows = await this.db` - SELECT * FROM players WHERE id = ${id} - `; - if (rows.length === 0) return null; - return this.rowToPlayer(rows[0] as PlayerRow); - } - - async findByOsuId(osuId: number): Promise { - const rows = await this.db` - SELECT * FROM players WHERE osu_id = ${osuId} - `; - if (rows.length === 0) return null; - return this.rowToPlayer(rows[0] as PlayerRow); - } - - async findByDiscordId(discordId: string): Promise { - const rows = await this.db` - SELECT * FROM players WHERE discord_id = ${discordId} - `; - if (rows.length === 0) return null; - return this.rowToPlayer(rows[0] as PlayerRow); - } - - async save(player: Player): Promise { - await this.db` - INSERT INTO players (id, discord_id, osu_id, osu_name, display_name, rating, created_at) - VALUES (${player.id}, ${player.discordId}, ${player.osuId}, ${player.osuName}, ${player.displayName}, ${player.rating}, ${player.createdAt}) - ON CONFLICT (id) DO UPDATE SET - discord_id = EXCLUDED.discord_id, - osu_id = EXCLUDED.osu_id, - osu_name = EXCLUDED.osu_name, - display_name = EXCLUDED.display_name, - rating = EXCLUDED.rating - `; - } - - async updateRating(playerId: string, newRating: number): Promise { - await this.db` - UPDATE players SET rating = ${newRating} WHERE id = ${playerId} - `; - } - - async findAll(): Promise { - const rows = await this.db`SELECT * FROM players ORDER BY created_at DESC`; - return rows.map((row) => this.rowToPlayer(row as PlayerRow)); - } -} diff --git a/backend/src/infrastructure/database/PostgresQueueRepository.ts b/backend/src/infrastructure/database/PostgresQueueRepository.ts deleted file mode 100644 index b08b81e..0000000 --- a/backend/src/infrastructure/database/PostgresQueueRepository.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { SQL } from "bun"; -import type { IQueueRepository } from "../../domain/interfaces/IQueueRepository"; -import type { QueueEntry } from "../../domain/models/QueueEntry"; -import type { Team } from "../../domain/models/Team"; -import type { Player } from "../../domain/models/Player"; -import { getAverageRating } from "../../domain/models/Team"; - -interface PlayerRow { - id: string; - discord_id: string; - osu_id: number; - osu_name: string; - display_name: string; - rating: number; - created_at: Date; -} - -interface TeamRow { - id: string; - name: string; - captain_id: string; - created_at: Date; -} - -interface QueueEntryRow { - id: string; - team_id: string; - queue_type: string; - joined_at: Date; -} - -export class PostgresQueueRepository implements IQueueRepository { - constructor(private db: SQL) {} - - private rowToPlayer(row: PlayerRow): Player { - return { - id: row.id, - discordId: row.discord_id, - osuId: row.osu_id, - osuName: row.osu_name, - displayName: row.display_name, - rating: row.rating, - createdAt: row.created_at, - }; - } - - private async loadTeamPlayers(teamId: string): Promise { - const rows = await this.db` - SELECT p.* FROM players p - JOIN team_players tp ON tp.player_id = p.id - WHERE tp.team_id = ${teamId} - `; - return rows.map((row) => this.rowToPlayer(row as PlayerRow)); - } - - private async loadTeam(teamId: string): Promise { - const rows = await this.db`SELECT * FROM teams WHERE id = ${teamId}`; - if (rows.length === 0) return null; - - const row = rows[0] as TeamRow; - const players = await this.loadTeamPlayers(teamId); - const captain = players.find((p) => p.id === row.captain_id) ?? players[0]; - - return { - id: row.id, - name: row.name, - players, - captain, - createdAt: row.created_at, - }; - } - - async add(entry: QueueEntry): Promise { - await this.db` - INSERT INTO queue_entries (id, team_id, queue_type, joined_at) - VALUES (${entry.id}, ${entry.team.id}, ${entry.queueType}, ${entry.joinedAt}) - ON CONFLICT (team_id) DO UPDATE SET - queue_type = EXCLUDED.queue_type, - joined_at = EXCLUDED.joined_at - `; - } - - async remove(teamId: string): Promise { - await this.db`DELETE FROM queue_entries WHERE team_id = ${teamId}`; - } - - async findByTeamId(teamId: string): Promise { - const rows = await this.db`SELECT * FROM queue_entries WHERE team_id = ${teamId}`; - if (rows.length === 0) return null; - - const row = rows[0] as QueueEntryRow; - const team = await this.loadTeam(row.team_id); - if (!team) return null; - - return { - id: row.id, - team, - queueType: row.queue_type, - joinedAt: row.joined_at, - }; - } - - async findByType(queueType: string): Promise { - const rows = await this.db` - SELECT * FROM queue_entries - WHERE queue_type = ${queueType} - ORDER BY joined_at ASC - `; - - const entries: QueueEntry[] = []; - for (const row of rows as QueueEntryRow[]) { - const team = await this.loadTeam(row.team_id); - if (team) { - entries.push({ - id: row.id, - team, - queueType: row.queue_type, - joinedAt: row.joined_at, - }); - } - } - - return entries; - } - - async findClosestByRating( - rating: number, - queueType: string, - exclude?: string[] - ): Promise { - const entries = await this.findByType(queueType); - - let closest: QueueEntry | null = null; - let closestDiff = Infinity; - - for (const entry of entries) { - if (exclude?.includes(entry.team.id)) continue; - - const teamRating = getAverageRating(entry.team); - const diff = Math.abs(teamRating - rating); - - if (diff < closestDiff) { - closestDiff = diff; - closest = entry; - } - } - - return closest; - } - - async findAll(): Promise { - const rows = await this.db`SELECT * FROM queue_entries ORDER BY joined_at ASC`; - - const entries: QueueEntry[] = []; - for (const row of rows as QueueEntryRow[]) { - const team = await this.loadTeam(row.team_id); - if (team) { - entries.push({ - id: row.id, - team, - queueType: row.queue_type, - joinedAt: row.joined_at, - }); - } - } - - return entries; - } - - async clear(): Promise { - await this.db`DELETE FROM queue_entries`; - } -} diff --git a/backend/src/infrastructure/database/PostgresTeamRepository.ts b/backend/src/infrastructure/database/PostgresTeamRepository.ts deleted file mode 100644 index abd10ef..0000000 --- a/backend/src/infrastructure/database/PostgresTeamRepository.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { SQL } from "bun"; -import type { ITeamRepository } from "../../domain/interfaces/ITeamRepository"; -import type { Team } from "../../domain/models/Team"; -import type { Player } from "../../domain/models/Player"; - -interface TeamRow { - id: string; - name: string; - captain_id: string; - created_at: Date; -} - -interface PlayerRow { - id: string; - discord_id: string; - osu_id: number; - osu_name: string; - display_name: string; - rating: number; - created_at: Date; -} - -export class PostgresTeamRepository implements ITeamRepository { - constructor(private db: SQL) {} - - private rowToPlayer(row: PlayerRow): Player { - return { - id: row.id, - discordId: row.discord_id, - osuId: row.osu_id, - osuName: row.osu_name, - displayName: row.display_name, - rating: row.rating, - createdAt: row.created_at, - }; - } - - private async loadTeamPlayers(teamId: string): Promise { - const rows = await this.db` - SELECT p.* FROM players p - JOIN team_players tp ON tp.player_id = p.id - WHERE tp.team_id = ${teamId} - `; - return rows.map((row) => this.rowToPlayer(row as PlayerRow)); - } - - async findById(id: string): Promise { - const rows = await this.db`SELECT * FROM teams WHERE id = ${id}`; - if (rows.length === 0) return null; - - const row = rows[0] as TeamRow; - const players = await this.loadTeamPlayers(id); - const captain = players.find((p) => p.id === row.captain_id) ?? players[0]; - - return { - id: row.id, - name: row.name, - players, - captain, - createdAt: row.created_at, - }; - } - - async findByPlayer(playerId: string): Promise { - const teamRows = await this.db` - SELECT t.* FROM teams t - JOIN team_players tp ON tp.team_id = t.id - WHERE tp.player_id = ${playerId} - `; - - const teams: Team[] = []; - for (const row of teamRows as TeamRow[]) { - const players = await this.loadTeamPlayers(row.id); - const captain = players.find((p) => p.id === row.captain_id) ?? players[0]; - teams.push({ - id: row.id, - name: row.name, - players, - captain, - createdAt: row.created_at, - }); - } - - return teams; - } - - async save(team: Team): Promise { - await this.db` - INSERT INTO teams (id, name, captain_id, created_at) - VALUES (${team.id}, ${team.name}, ${team.captain.id}, ${team.createdAt}) - ON CONFLICT (id) DO UPDATE SET - name = EXCLUDED.name, - captain_id = EXCLUDED.captain_id - `; - - await this.db`DELETE FROM team_players WHERE team_id = ${team.id}`; - - for (const player of team.players) { - await this.db` - INSERT INTO team_players (team_id, player_id) - VALUES (${team.id}, ${player.id}) - `; - } - } - - async delete(teamId: string): Promise { - await this.db`DELETE FROM teams WHERE id = ${teamId}`; - } - - async findAll(): Promise { - const teamRows = await this.db`SELECT * FROM teams ORDER BY created_at DESC`; - const teams: Team[] = []; - - for (const row of teamRows as TeamRow[]) { - const players = await this.loadTeamPlayers(row.id); - const captain = players.find((p) => p.id === row.captain_id) ?? players[0]; - teams.push({ - id: row.id, - name: row.name, - players, - captain, - createdAt: row.created_at, - }); - } - - return teams; - } -} diff --git a/backend/src/infrastructure/database/index.ts b/backend/src/infrastructure/database/index.ts deleted file mode 100644 index b90cb54..0000000 --- a/backend/src/infrastructure/database/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./PostgresPlayerRepository"; -export * from "./PostgresTeamRepository"; -export * from "./PostgresMatchRepository"; -export * from "./PostgresQueueRepository"; diff --git a/backend/src/infrastructure/database/migrations/001_initial.sql b/backend/src/infrastructure/database/migrations/001_initial.sql deleted file mode 100644 index 109a6bf..0000000 --- a/backend/src/infrastructure/database/migrations/001_initial.sql +++ /dev/null @@ -1,98 +0,0 @@ --- Initial database schema for Vash Esports Backend - -CREATE TABLE IF NOT EXISTS players ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - discord_id VARCHAR(255) UNIQUE NOT NULL, - osu_id INTEGER UNIQUE NOT NULL, - osu_name VARCHAR(255) NOT NULL, - display_name VARCHAR(255) NOT NULL, - rating INTEGER DEFAULT 1000, - created_at TIMESTAMP DEFAULT NOW() -); - -CREATE TABLE IF NOT EXISTS teams ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(255) NOT NULL, - captain_id UUID REFERENCES players(id), - created_at TIMESTAMP DEFAULT NOW() -); - -CREATE TABLE IF NOT EXISTS team_players ( - team_id UUID REFERENCES teams(id) ON DELETE CASCADE, - player_id UUID REFERENCES players(id) ON DELETE CASCADE, - PRIMARY KEY (team_id, player_id) -); - -CREATE TABLE IF NOT EXISTS matches ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - protocol VARCHAR(100) NOT NULL, - state VARCHAR(50) NOT NULL, - lobby_id VARCHAR(255), - winner_team_id UUID REFERENCES teams(id), - created_at TIMESTAMP DEFAULT NOW(), - started_at TIMESTAMP, - ended_at TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS match_teams ( - match_id UUID REFERENCES matches(id) ON DELETE CASCADE, - team_id UUID REFERENCES teams(id) ON DELETE CASCADE, - team_order INTEGER NOT NULL, - PRIMARY KEY (match_id, team_id) -); - -CREATE TABLE IF NOT EXISTS match_rolls ( - match_id UUID REFERENCES matches(id) ON DELETE CASCADE, - team_id UUID REFERENCES teams(id) ON DELETE CASCADE, - roll_value INTEGER NOT NULL, - PRIMARY KEY (match_id, team_id) -); - -CREATE TABLE IF NOT EXISTS match_bans ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - match_id UUID REFERENCES matches(id) ON DELETE CASCADE, - team_id UUID REFERENCES teams(id) ON DELETE CASCADE, - map_id INTEGER NOT NULL, - ban_order INTEGER NOT NULL -); - -CREATE TABLE IF NOT EXISTS match_picks ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - match_id UUID REFERENCES matches(id) ON DELETE CASCADE, - map_id INTEGER NOT NULL, - pick_order INTEGER NOT NULL -); - -CREATE TABLE IF NOT EXISTS match_scores ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - match_id UUID REFERENCES matches(id) ON DELETE CASCADE, - map_id INTEGER NOT NULL, - winner_team_id UUID REFERENCES teams(id), - map_order INTEGER NOT NULL -); - -CREATE TABLE IF NOT EXISTS player_scores ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - match_score_id UUID REFERENCES match_scores(id) ON DELETE CASCADE, - player_id UUID REFERENCES players(id), - score INTEGER NOT NULL, - accuracy DECIMAL(5,2), - max_combo INTEGER, - misses INTEGER, - mods TEXT[] -); - -CREATE TABLE IF NOT EXISTS queue_entries ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - team_id UUID REFERENCES teams(id) ON DELETE CASCADE UNIQUE, - queue_type VARCHAR(50) NOT NULL, - joined_at TIMESTAMP DEFAULT NOW() -); - --- Indexes for common queries -CREATE INDEX IF NOT EXISTS idx_players_osu_id ON players(osu_id); -CREATE INDEX IF NOT EXISTS idx_players_discord_id ON players(discord_id); -CREATE INDEX IF NOT EXISTS idx_matches_state ON matches(state) WHERE state NOT IN ('completed', 'cancelled'); -CREATE INDEX IF NOT EXISTS idx_matches_lobby_id ON matches(lobby_id) WHERE lobby_id IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_queue_entries_type ON queue_entries(queue_type, joined_at); -CREATE INDEX IF NOT EXISTS idx_team_players_player ON team_players(player_id); diff --git a/backend/src/infrastructure/discord/DiscordService.ts b/backend/src/infrastructure/discord/DiscordService.ts deleted file mode 100644 index 3f1644a..0000000 --- a/backend/src/infrastructure/discord/DiscordService.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { Client, GatewayIntentBits, EmbedBuilder, type TextChannel } from "discord.js"; -import type { INotificationService } from "../../domain/interfaces/INotificationService"; -import type { Match } from "../../domain/models/Match"; -import type { Team } from "../../domain/models/Team"; -import type { MatchScore } from "../../domain/models/Score"; -import type { EventBus } from "../../application/events/EventBus"; -import { getTeamWins } from "../../domain/models/Match"; - -export interface DiscordConfig { - token: string; - guildId: string; - announcementChannelId: string; - matchResultsChannelId: string; -} - -export class DiscordService implements INotificationService { - private client: Client; - private ready = false; - - constructor( - private config: DiscordConfig, - private eventBus?: EventBus - ) { - this.client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, - ], - }); - - this.setupClient(); - } - - private async setupClient(): Promise { - this.client.on("ready", () => { - console.log(`[DiscordService] Logged in as ${this.client.user?.tag}`); - this.ready = true; - }); - - this.client.on("error", (error) => { - console.error("[DiscordService] Client error:", error); - }); - - if (this.config.token) { - try { - await this.client.login(this.config.token); - } catch (error) { - console.error("[DiscordService] Failed to login:", error); - } - } - } - - async announceMatch(match: Match): Promise { - if (!this.ready) { - console.log("[DiscordService] Not ready, skipping announcement"); - return; - } - - const channel = await this.client.channels.fetch( - this.config.announcementChannelId - ); - if (!channel || !channel.isTextBased()) return; - - const embed = new EmbedBuilder() - .setTitle("New Match Starting!") - .setDescription( - `**${match.teams[0].name}** vs **${match.teams[1].name}**` - ) - .addFields( - { - name: match.teams[0].name, - value: match.teams[0].players.map((p) => p.displayName).join("\n"), - inline: true, - }, - { - name: match.teams[1].name, - value: match.teams[1].players.map((p) => p.displayName).join("\n"), - inline: true, - } - ) - .setColor(0x00ae86) - .setTimestamp(); - - await (channel as TextChannel).send({ embeds: [embed] }); - } - - async announceMatchResult(match: Match, winner: Team): Promise { - if (!this.ready) { - console.log("[DiscordService] Not ready, skipping result announcement"); - return; - } - - const channel = await this.client.channels.fetch( - this.config.matchResultsChannelId - ); - if (!channel || !channel.isTextBased()) return; - - const team1Wins = getTeamWins(match, match.teams[0].id); - const team2Wins = getTeamWins(match, match.teams[1].id); - - const embed = new EmbedBuilder() - .setTitle("Match Completed!") - .setDescription(`**${winner.name}** wins!`) - .addFields({ - name: "Final Score", - value: `${match.teams[0].name} **${team1Wins}** - **${team2Wins}** ${match.teams[1].name}`, - }) - .setColor(winner.id === match.teams[0].id ? 0x3498db : 0xe74c3c) - .setTimestamp(); - - await (channel as TextChannel).send({ embeds: [embed] }); - } - - async notifyPlayer(playerId: string, message: string): Promise { - if (!this.ready) { - console.log("[DiscordService] Not ready, skipping player notification"); - return; - } - - try { - const user = await this.client.users.fetch(playerId); - await user.send(message); - } catch (error) { - console.error(`[DiscordService] Failed to DM player ${playerId}:`, error); - } - } - - async notifyTeam(teamId: string, message: string): Promise { - // In a real implementation, you'd look up the team's players - // and notify each one. For now, just log. - console.log(`[DiscordService] Team ${teamId}: ${message}`); - } - - async announceMapResult(match: Match, scores: MatchScore): Promise { - if (!this.ready) { - console.log("[DiscordService] Not ready, skipping map result"); - return; - } - - const channel = await this.client.channels.fetch( - this.config.announcementChannelId - ); - if (!channel || !channel.isTextBased()) return; - - const winner = match.teams.find((t) => t.id === scores.winner); - const team1Score = scores.teamScores.get(match.teams[0].id) ?? 0; - const team2Score = scores.teamScores.get(match.teams[1].id) ?? 0; - const team1Wins = getTeamWins(match, match.teams[0].id); - const team2Wins = getTeamWins(match, match.teams[1].id); - - const embed = new EmbedBuilder() - .setTitle("Map Complete") - .setDescription(`**${winner?.name}** wins the map!`) - .addFields( - { - name: "Map Score", - value: `${match.teams[0].name}: ${team1Score.toLocaleString()}\n${match.teams[1].name}: ${team2Score.toLocaleString()}`, - }, - { - name: "Match Score", - value: `${match.teams[0].name} **${team1Wins}** - **${team2Wins}** ${match.teams[1].name}`, - } - ) - .setColor(0x9b59b6) - .setTimestamp(); - - await (channel as TextChannel).send({ embeds: [embed] }); - } - - async destroy(): Promise { - await this.client.destroy(); - } -} diff --git a/backend/src/infrastructure/discord/index.ts b/backend/src/infrastructure/discord/index.ts deleted file mode 100644 index dbedea0..0000000 --- a/backend/src/infrastructure/discord/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./DiscordService"; diff --git a/backend/src/infrastructure/mocks/InMemoryMatchRepository.ts b/backend/src/infrastructure/mocks/InMemoryMatchRepository.ts deleted file mode 100644 index f95a5f6..0000000 --- a/backend/src/infrastructure/mocks/InMemoryMatchRepository.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { IMatchRepository } from "../../domain/interfaces/IMatchRepository"; -import type { Match } from "../../domain/models/Match"; -import { MatchState, isActiveState } from "../../domain/models/MatchState"; - -export class InMemoryMatchRepository implements IMatchRepository { - private matches: Map = new Map(); - - async findById(id: string): Promise { - return this.matches.get(id) ?? null; - } - - async findActive(): Promise { - const result: Match[] = []; - for (const match of this.matches.values()) { - if (isActiveState(match.state)) { - result.push(match); - } - } - return result; - } - - async findByPlayer(playerId: string, limit?: number): Promise { - const result: Match[] = []; - for (const match of this.matches.values()) { - const hasPlayer = match.teams.some((t) => - t.players.some((p) => p.id === playerId) - ); - if (hasPlayer) { - result.push(match); - if (limit && result.length >= limit) break; - } - } - return result; - } - - async findByLobbyId(lobbyId: string): Promise { - for (const match of this.matches.values()) { - if (match.lobbyId === lobbyId) { - return match; - } - } - return null; - } - - async save(match: Match): Promise { - this.matches.set(match.id, match); - } - - async updateState(matchId: string, state: MatchState): Promise { - const match = this.matches.get(matchId); - if (match) { - match.state = state; - } - } - - async findAll(): Promise { - return Array.from(this.matches.values()); - } - - clear(): void { - this.matches.clear(); - } -} diff --git a/backend/src/infrastructure/mocks/InMemoryPlayerRepository.ts b/backend/src/infrastructure/mocks/InMemoryPlayerRepository.ts deleted file mode 100644 index 138df03..0000000 --- a/backend/src/infrastructure/mocks/InMemoryPlayerRepository.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { IPlayerRepository } from "../../domain/interfaces/IPlayerRepository"; -import type { Player } from "../../domain/models/Player"; - -export class InMemoryPlayerRepository implements IPlayerRepository { - private players: Map = new Map(); - - async findById(id: string): Promise { - return this.players.get(id) ?? null; - } - - async findByOsuId(osuId: number): Promise { - for (const player of this.players.values()) { - if (player.osuId === osuId) { - return player; - } - } - return null; - } - - async findByDiscordId(discordId: string): Promise { - for (const player of this.players.values()) { - if (player.discordId === discordId) { - return player; - } - } - return null; - } - - async save(player: Player): Promise { - this.players.set(player.id, player); - } - - async updateRating(playerId: string, newRating: number): Promise { - const player = this.players.get(playerId); - if (player) { - player.rating = newRating; - } - } - - async findAll(): Promise { - return Array.from(this.players.values()); - } - - clear(): void { - this.players.clear(); - } -} diff --git a/backend/src/infrastructure/mocks/InMemoryQueueRepository.ts b/backend/src/infrastructure/mocks/InMemoryQueueRepository.ts deleted file mode 100644 index af402b9..0000000 --- a/backend/src/infrastructure/mocks/InMemoryQueueRepository.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { IQueueRepository } from "../../domain/interfaces/IQueueRepository"; -import type { QueueEntry } from "../../domain/models/QueueEntry"; -import { getAverageRating } from "../../domain/models/Team"; - -export class InMemoryQueueRepository implements IQueueRepository { - private entries: Map = new Map(); - - async add(entry: QueueEntry): Promise { - this.entries.set(entry.team.id, entry); - } - - async remove(teamId: string): Promise { - this.entries.delete(teamId); - } - - async findByTeamId(teamId: string): Promise { - return this.entries.get(teamId) ?? null; - } - - async findByType(queueType: string): Promise { - const result: QueueEntry[] = []; - for (const entry of this.entries.values()) { - if (entry.queueType === queueType) { - result.push(entry); - } - } - return result.sort((a, b) => a.joinedAt.getTime() - b.joinedAt.getTime()); - } - - async findClosestByRating( - rating: number, - queueType: string, - exclude?: string[] - ): Promise { - let closest: QueueEntry | null = null; - let closestDiff = Infinity; - - for (const entry of this.entries.values()) { - if (entry.queueType !== queueType) continue; - if (exclude?.includes(entry.team.id)) continue; - - const teamRating = getAverageRating(entry.team); - const diff = Math.abs(teamRating - rating); - - if (diff < closestDiff) { - closestDiff = diff; - closest = entry; - } - } - - return closest; - } - - async findAll(): Promise { - return Array.from(this.entries.values()); - } - - async clear(): Promise { - this.entries.clear(); - } -} diff --git a/backend/src/infrastructure/mocks/InMemoryTeamRepository.ts b/backend/src/infrastructure/mocks/InMemoryTeamRepository.ts deleted file mode 100644 index 9025482..0000000 --- a/backend/src/infrastructure/mocks/InMemoryTeamRepository.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ITeamRepository } from "../../domain/interfaces/ITeamRepository"; -import type { Team } from "../../domain/models/Team"; - -export class InMemoryTeamRepository implements ITeamRepository { - private teams: Map = new Map(); - - async findById(id: string): Promise { - return this.teams.get(id) ?? null; - } - - async findByPlayer(playerId: string): Promise { - const result: Team[] = []; - for (const team of this.teams.values()) { - if (team.players.some((p) => p.id === playerId)) { - result.push(team); - } - } - return result; - } - - async save(team: Team): Promise { - this.teams.set(team.id, team); - } - - async delete(teamId: string): Promise { - this.teams.delete(teamId); - } - - async findAll(): Promise { - return Array.from(this.teams.values()); - } - - clear(): void { - this.teams.clear(); - } -} diff --git a/backend/src/infrastructure/mocks/MockGameService.ts b/backend/src/infrastructure/mocks/MockGameService.ts deleted file mode 100644 index 37788c0..0000000 --- a/backend/src/infrastructure/mocks/MockGameService.ts +++ /dev/null @@ -1,136 +0,0 @@ -import type { IGameService } from "../../domain/interfaces/IGameService"; -import type { Match } from "../../domain/models/Match"; -import type { Player } from "../../domain/models/Player"; -import type { Score } from "../../domain/models/Score"; - -interface MockLobby { - id: string; - matchId: string; - players: Set; - messages: string[]; - currentMap?: number; -} - -export class MockGameService implements IGameService { - public lobbies: Map = new Map(); - public messageLog: Array<{ lobbyId: string; message: string }> = []; - - private playerJoinCallbacks: Array<(osuId: number, lobbyId: string) => void> = []; - private playerLeaveCallbacks: Array<(osuId: number, lobbyId: string) => void> = []; - private matchStartCallbacks: Array<(lobbyId: string) => void> = []; - private matchFinishCallbacks: Array<(lobbyId: string, scores: Score[]) => void> = []; - private rollCallbacks: Array<(osuId: number, lobbyId: string, roll: number) => void> = []; - - async createLobby(match: Match): Promise { - const lobbyId = `mock-lobby-${match.id}`; - this.lobbies.set(lobbyId, { - id: lobbyId, - matchId: match.id, - players: new Set(), - messages: [], - }); - return lobbyId; - } - - async closeLobby(lobbyId: string): Promise { - this.lobbies.delete(lobbyId); - } - - async invitePlayer(player: Player, lobbyId: string): Promise { - const lobby = this.lobbies.get(lobbyId); - if (lobby) { - lobby.messages.push(`Invited ${player.osuName}`); - } - } - - async sendMessage(lobbyId: string, message: string): Promise { - const lobby = this.lobbies.get(lobbyId); - if (lobby) { - lobby.messages.push(message); - } - this.messageLog.push({ lobbyId, message }); - } - - async setMap(lobbyId: string, mapId: number): Promise { - const lobby = this.lobbies.get(lobbyId); - if (lobby) { - lobby.currentMap = mapId; - lobby.messages.push(`Changed map to ${mapId}`); - } - } - - async startMatch(lobbyId: string): Promise { - const lobby = this.lobbies.get(lobbyId); - if (lobby) { - lobby.messages.push("Match started"); - } - } - - onPlayerJoin(callback: (osuId: number, lobbyId: string) => void): void { - this.playerJoinCallbacks.push(callback); - } - - onPlayerLeave(callback: (osuId: number, lobbyId: string) => void): void { - this.playerLeaveCallbacks.push(callback); - } - - onMatchStart(callback: (lobbyId: string) => void): void { - this.matchStartCallbacks.push(callback); - } - - onMatchFinish(callback: (lobbyId: string, scores: Score[]) => void): void { - this.matchFinishCallbacks.push(callback); - } - - onRoll(callback: (osuId: number, lobbyId: string, roll: number) => void): void { - this.rollCallbacks.push(callback); - } - - simulatePlayerJoin(lobbyId: string, osuId: number): void { - const lobby = this.lobbies.get(lobbyId); - if (lobby) { - lobby.players.add(osuId); - } - for (const cb of this.playerJoinCallbacks) { - cb(osuId, lobbyId); - } - } - - simulatePlayerLeave(lobbyId: string, osuId: number): void { - const lobby = this.lobbies.get(lobbyId); - if (lobby) { - lobby.players.delete(osuId); - } - for (const cb of this.playerLeaveCallbacks) { - cb(osuId, lobbyId); - } - } - - simulateRoll(lobbyId: string, osuId: number, roll: number): void { - for (const cb of this.rollCallbacks) { - cb(osuId, lobbyId, roll); - } - } - - simulateMatchStart(lobbyId: string): void { - for (const cb of this.matchStartCallbacks) { - cb(lobbyId); - } - } - - simulateMatchFinish(lobbyId: string, scores: Score[]): void { - for (const cb of this.matchFinishCallbacks) { - cb(lobbyId, scores); - } - } - - clear(): void { - this.lobbies.clear(); - this.messageLog = []; - this.playerJoinCallbacks = []; - this.playerLeaveCallbacks = []; - this.matchStartCallbacks = []; - this.matchFinishCallbacks = []; - this.rollCallbacks = []; - } -} diff --git a/backend/src/infrastructure/mocks/MockNotificationService.ts b/backend/src/infrastructure/mocks/MockNotificationService.ts deleted file mode 100644 index 98d1ce4..0000000 --- a/backend/src/infrastructure/mocks/MockNotificationService.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { INotificationService } from "../../domain/interfaces/INotificationService"; -import type { Match } from "../../domain/models/Match"; -import type { Team } from "../../domain/models/Team"; -import type { MatchScore } from "../../domain/models/Score"; - -interface Notification { - type: string; - targetId?: string; - message?: string; - match?: Match; - winner?: Team; - scores?: MatchScore; -} - -export class MockNotificationService implements INotificationService { - public notifications: Notification[] = []; - public announcements: Match[] = []; - public playerNotifications: Array<{ playerId: string; message: string }> = []; - public teamNotifications: Array<{ teamId: string; message: string }> = []; - - async announceMatch(match: Match): Promise { - this.announcements.push(match); - this.notifications.push({ type: "match_announced", match }); - } - - async announceMatchResult(match: Match, winner: Team): Promise { - this.notifications.push({ type: "match_result", match, winner }); - } - - async notifyPlayer(playerId: string, message: string): Promise { - this.playerNotifications.push({ playerId, message }); - this.notifications.push({ type: "player_notification", targetId: playerId, message }); - } - - async notifyTeam(teamId: string, message: string): Promise { - this.teamNotifications.push({ teamId, message }); - this.notifications.push({ type: "team_notification", targetId: teamId, message }); - } - - async announceMapResult(match: Match, scores: MatchScore): Promise { - this.notifications.push({ type: "map_result", match, scores }); - } - - clear(): void { - this.notifications = []; - this.announcements = []; - this.playerNotifications = []; - this.teamNotifications = []; - } -} diff --git a/backend/src/infrastructure/mocks/index.ts b/backend/src/infrastructure/mocks/index.ts deleted file mode 100644 index b036d12..0000000 --- a/backend/src/infrastructure/mocks/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./InMemoryPlayerRepository"; -export * from "./InMemoryTeamRepository"; -export * from "./InMemoryMatchRepository"; -export * from "./InMemoryQueueRepository"; -export * from "./MockGameService"; -export * from "./MockNotificationService"; diff --git a/backend/src/infrastructure/osu/OsuService.ts b/backend/src/infrastructure/osu/OsuService.ts deleted file mode 100644 index c4d4ec5..0000000 --- a/backend/src/infrastructure/osu/OsuService.ts +++ /dev/null @@ -1,187 +0,0 @@ -import type { IGameService } from "../../domain/interfaces/IGameService"; -import type { Match } from "../../domain/models/Match"; -import type { Player } from "../../domain/models/Player"; -import type { Score } from "../../domain/models/Score"; - -type PlayerJoinCallback = (osuId: number, lobbyId: string) => void; -type PlayerLeaveCallback = (osuId: number, lobbyId: string) => void; -type MatchStartCallback = (lobbyId: string) => void; -type MatchFinishCallback = (lobbyId: string, scores: Score[]) => void; -type RollCallback = (osuId: number, lobbyId: string, roll: number) => void; - -export interface OsuApiConfig { - clientId: string; - clientSecret: string; -} - -export interface OsuIrcConfig { - username: string; - password: string; - server?: string; - port?: number; -} - -export class OsuService implements IGameService { - private playerJoinCallbacks: PlayerJoinCallback[] = []; - private playerLeaveCallbacks: PlayerLeaveCallback[] = []; - private matchStartCallbacks: MatchStartCallback[] = []; - private matchFinishCallbacks: MatchFinishCallback[] = []; - private rollCallbacks: RollCallback[] = []; - - private lobbies: Map = new Map(); - - constructor( - private apiConfig: OsuApiConfig, - private ircConfig: OsuIrcConfig - ) { - this.connectIrc(); - } - - private async connectIrc(): Promise { - // TODO: Implement IRC connection using Bun's WebSocket - // Connect to irc.ppy.sh - // Authenticate with BanchoBot - console.log("[OsuService] IRC connection not yet implemented"); - } - - async createLobby(match: Match): Promise { - const lobbyName = `VASH: ${match.teams.map((t) => t.name).join(" vs ")}`; - // TODO: Send IRC command to create lobby - // !mp make - // Parse response to get lobby ID - - const lobbyId = `osu_${match.id.slice(0, 8)}`; - this.lobbies.set(lobbyId, { - matchId: match.id, - channel: `#mp_${lobbyId}`, - }); - - console.log(`[OsuService] Created lobby: ${lobbyId}`); - return lobbyId; - } - - async closeLobby(lobbyId: string): Promise { - // TODO: Send IRC command to close lobby - // !mp close - this.lobbies.delete(lobbyId); - console.log(`[OsuService] Closed lobby: ${lobbyId}`); - } - - async invitePlayer(player: Player, lobbyId: string): Promise { - const lobby = this.lobbies.get(lobbyId); - if (!lobby) { - throw new Error(`Lobby ${lobbyId} not found`); - } - // TODO: Send IRC command - // !mp invite - console.log(`[OsuService] Invited ${player.osuName} to ${lobbyId}`); - } - - async sendMessage(lobbyId: string, message: string): Promise { - const lobby = this.lobbies.get(lobbyId); - if (!lobby) { - throw new Error(`Lobby ${lobbyId} not found`); - } - // TODO: Send IRC message to lobby channel - console.log(`[OsuService] [${lobbyId}] ${message}`); - } - - async setMap(lobbyId: string, mapId: number): Promise { - const lobby = this.lobbies.get(lobbyId); - if (!lobby) { - throw new Error(`Lobby ${lobbyId} not found`); - } - // TODO: Send IRC command - // !mp map - console.log(`[OsuService] Set map ${mapId} in ${lobbyId}`); - } - - async startMatch(lobbyId: string): Promise { - const lobby = this.lobbies.get(lobbyId); - if (!lobby) { - throw new Error(`Lobby ${lobbyId} not found`); - } - // TODO: Send IRC command - // !mp start - console.log(`[OsuService] Started match in ${lobbyId}`); - } - - onPlayerJoin(callback: PlayerJoinCallback): void { - this.playerJoinCallbacks.push(callback); - } - - onPlayerLeave(callback: PlayerLeaveCallback): void { - this.playerLeaveCallbacks.push(callback); - } - - onMatchStart(callback: MatchStartCallback): void { - this.matchStartCallbacks.push(callback); - } - - onMatchFinish(callback: MatchFinishCallback): void { - this.matchFinishCallbacks.push(callback); - } - - onRoll(callback: RollCallback): void { - this.rollCallbacks.push(callback); - } - - // IRC message parsing - called when messages are received - private handleIrcMessage(channel: string, user: string, message: string): void { - // Find lobby by channel - let lobbyId: string | null = null; - for (const [id, lobby] of this.lobbies) { - if (lobby.channel === channel) { - lobbyId = id; - break; - } - } - if (!lobbyId) return; - - // Parse BanchoBot messages - if (user === "BanchoBot") { - // Player joined: " joined in slot X" - const joinMatch = message.match(/^(\S+) joined in slot \d+/); - if (joinMatch) { - // TODO: Look up osuId from username - // For now, we'd need an API call or cache - console.log(`[OsuService] Player joined: ${joinMatch[1]}`); - } - - // Match finished - parse scores - // Format varies, typically shows all player scores - if (message.includes("The match has finished")) { - // TODO: Parse scores from recent messages or API - console.log(`[OsuService] Match finished in ${lobbyId}`); - } - } - - // Parse roll command: " rolls X point(s)" - const rollMatch = message.match(/^(\S+) rolls (\d+) point/); - if (rollMatch) { - const username = rollMatch[1]; - const roll = parseInt(rollMatch[2]); - // TODO: Look up osuId from username - console.log(`[OsuService] ${username} rolled ${roll}`); - } - } - - // Expose for testing - _triggerPlayerJoin(osuId: number, lobbyId: string): void { - for (const cb of this.playerJoinCallbacks) { - cb(osuId, lobbyId); - } - } - - _triggerRoll(osuId: number, lobbyId: string, roll: number): void { - for (const cb of this.rollCallbacks) { - cb(osuId, lobbyId, roll); - } - } - - _triggerMatchFinish(lobbyId: string, scores: Score[]): void { - for (const cb of this.matchFinishCallbacks) { - cb(lobbyId, scores); - } - } -} diff --git a/backend/src/infrastructure/osu/index.ts b/backend/src/infrastructure/osu/index.ts deleted file mode 100644 index c2eeb6c..0000000 --- a/backend/src/infrastructure/osu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./OsuService"; diff --git a/backend/src/infrastructure/websocket/WebSocketService.ts b/backend/src/infrastructure/websocket/WebSocketService.ts deleted file mode 100644 index 16206d0..0000000 --- a/backend/src/infrastructure/websocket/WebSocketService.ts +++ /dev/null @@ -1,220 +0,0 @@ -import type { ServerWebSocket } from "bun"; -import type { EventBus } from "../../application/events/EventBus"; -import type { DomainEvent } from "../../domain/events/DomainEvent"; - -interface WebSocketData { - id: string; - subscribedMatches: Set; -} - -type WebSocketClient = ServerWebSocket; - -export interface ClientMessage { - type: string; - matchId?: string; - payload?: unknown; -} - -export class WebSocketService { - private clients: Map = new Map(); - private matchSubscriptions: Map> = new Map(); - - constructor(private eventBus: EventBus) { - this.setupEventSubscriptions(); - } - - private setupEventSubscriptions(): void { - const eventsToForward = [ - "match.created", - "match.state_changed", - "match.player_joined", - "match.roll_completed", - "match.map_picked", - "match.map_banned", - "match.map_started", - "match.map_completed", - "match.completed", - "match.cancelled", - "queue.team_joined", - "queue.team_left", - "queue.match_found", - ]; - - for (const eventType of eventsToForward) { - this.eventBus.subscribe(eventType as any, (event: DomainEvent) => { - this.broadcastEvent(event); - }); - } - } - - handleOpen(ws: WebSocketClient): void { - const clientId = crypto.randomUUID(); - ws.data = { - id: clientId, - subscribedMatches: new Set(), - }; - this.clients.set(clientId, ws); - console.log(`[WebSocket] Client connected: ${clientId}`); - - ws.send(JSON.stringify({ type: "connected", clientId })); - } - - handleMessage(ws: WebSocketClient, message: string | Buffer): void { - try { - const msg: ClientMessage = - typeof message === "string" ? JSON.parse(message) : JSON.parse(message.toString()); - - switch (msg.type) { - case "subscribe": - if (msg.matchId) { - this.subscribeToMatch(ws, msg.matchId); - } - break; - - case "unsubscribe": - if (msg.matchId) { - this.unsubscribeFromMatch(ws, msg.matchId); - } - break; - - case "ping": - ws.send(JSON.stringify({ type: "pong" })); - break; - - default: - console.log(`[WebSocket] Unknown message type: ${msg.type}`); - } - } catch (error) { - console.error("[WebSocket] Failed to parse message:", error); - } - } - - handleClose(ws: WebSocketClient): void { - const clientId = ws.data?.id; - if (clientId) { - // Clean up subscriptions - for (const matchId of ws.data.subscribedMatches) { - const subs = this.matchSubscriptions.get(matchId); - if (subs) { - subs.delete(clientId); - if (subs.size === 0) { - this.matchSubscriptions.delete(matchId); - } - } - } - - this.clients.delete(clientId); - console.log(`[WebSocket] Client disconnected: ${clientId}`); - } - } - - subscribeToMatch(ws: WebSocketClient, matchId: string): void { - const clientId = ws.data?.id; - if (!clientId) return; - - ws.data.subscribedMatches.add(matchId); - - if (!this.matchSubscriptions.has(matchId)) { - this.matchSubscriptions.set(matchId, new Set()); - } - this.matchSubscriptions.get(matchId)!.add(clientId); - - ws.send(JSON.stringify({ type: "subscribed", matchId })); - console.log(`[WebSocket] Client ${clientId} subscribed to match ${matchId}`); - } - - unsubscribeFromMatch(ws: WebSocketClient, matchId: string): void { - const clientId = ws.data?.id; - if (!clientId) return; - - ws.data.subscribedMatches.delete(matchId); - - const subs = this.matchSubscriptions.get(matchId); - if (subs) { - subs.delete(clientId); - if (subs.size === 0) { - this.matchSubscriptions.delete(matchId); - } - } - - ws.send(JSON.stringify({ type: "unsubscribed", matchId })); - } - - private broadcastEvent(event: DomainEvent): void { - const payload = event.payload as any; - const matchId = payload?.match?.id; - - const message = JSON.stringify({ - type: "event", - eventType: event.type, - timestamp: event.timestamp.toISOString(), - payload: this.serializePayload(payload), - }); - - if (matchId) { - // Send to match subscribers - const subscribers = this.matchSubscriptions.get(matchId); - if (subscribers) { - for (const clientId of subscribers) { - const client = this.clients.get(clientId); - if (client) { - client.send(message); - } - } - } - } else { - // Broadcast to all clients - for (const client of this.clients.values()) { - client.send(message); - } - } - } - - private serializePayload(payload: any): any { - if (!payload) return payload; - - // Handle Maps - if (payload instanceof Map) { - return Object.fromEntries(payload); - } - - // Handle Sets - if (payload instanceof Set) { - return Array.from(payload); - } - - // Handle objects with Maps/Sets - if (typeof payload === "object") { - const result: any = {}; - for (const [key, value] of Object.entries(payload)) { - if (value instanceof Map) { - result[key] = Object.fromEntries(value); - } else if (value instanceof Set) { - result[key] = Array.from(value); - } else if (typeof value === "object" && value !== null) { - result[key] = this.serializePayload(value); - } else { - result[key] = value; - } - } - return result; - } - - return payload; - } - - broadcast(message: object): void { - const msg = JSON.stringify(message); - for (const client of this.clients.values()) { - client.send(msg); - } - } - - getConnectedCount(): number { - return this.clients.size; - } - - getMatchSubscriberCount(matchId: string): number { - return this.matchSubscriptions.get(matchId)?.size ?? 0; - } -} diff --git a/backend/src/infrastructure/websocket/index.ts b/backend/src/infrastructure/websocket/index.ts deleted file mode 100644 index 6be4791..0000000 --- a/backend/src/infrastructure/websocket/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./WebSocketService"; diff --git a/backend/src/presentation/api/routes.ts b/backend/src/presentation/api/routes.ts deleted file mode 100644 index e69de29..0000000 diff --git a/backend/src/testing/TestHarness.ts b/backend/src/testing/TestHarness.ts deleted file mode 100644 index 316bf6d..0000000 --- a/backend/src/testing/TestHarness.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { EventBus } from "../application/events/EventBus"; -import { MatchService } from "../application/services/MatchService"; -import { QueueService } from "../application/services/QueueService"; -import { ProtocolRegistry } from "../domain/protocols/ProtocolRegistry"; -import { StandardOsuProtocol } from "../domain/protocols/StandardOsuProtocol"; -import { HeadToHeadProtocol } from "../domain/protocols/HeadToHeadProtocol"; -import { InMemoryPlayerRepository } from "../infrastructure/mocks/InMemoryPlayerRepository"; -import { InMemoryTeamRepository } from "../infrastructure/mocks/InMemoryTeamRepository"; -import { InMemoryMatchRepository } from "../infrastructure/mocks/InMemoryMatchRepository"; -import { InMemoryQueueRepository } from "../infrastructure/mocks/InMemoryQueueRepository"; -import { MockGameService } from "../infrastructure/mocks/MockGameService"; -import { MockNotificationService } from "../infrastructure/mocks/MockNotificationService"; -import { type Player, createPlayer } from "../domain/models/Player"; -import { type Team, createTeam } from "../domain/models/Team"; -import type { Match } from "../domain/models/Match"; -import type { Score } from "../domain/models/Score"; -import { MatchState } from "../domain/models/MatchState"; -import type { DomainEvent } from "../domain/events/DomainEvent"; - -export class TestHarness { - public eventBus: EventBus; - public mockGameService: MockGameService; - public mockNotificationService: MockNotificationService; - public matchService: MatchService; - public queueService: QueueService; - public protocolRegistry: ProtocolRegistry; - - public playerRepo: InMemoryPlayerRepository; - public teamRepo: InMemoryTeamRepository; - public matchRepo: InMemoryMatchRepository; - public queueRepo: InMemoryQueueRepository; - - public events: DomainEvent[] = []; - - private playerCounter = 1; - private teamCounter = 1; - - constructor() { - this.eventBus = new EventBus(); - this.mockGameService = new MockGameService(); - this.mockNotificationService = new MockNotificationService(); - - this.playerRepo = new InMemoryPlayerRepository(); - this.teamRepo = new InMemoryTeamRepository(); - this.matchRepo = new InMemoryMatchRepository(); - this.queueRepo = new InMemoryQueueRepository(); - - this.protocolRegistry = new ProtocolRegistry(); - this.protocolRegistry.register(new StandardOsuProtocol()); - this.protocolRegistry.register(new HeadToHeadProtocol()); - - this.matchService = new MatchService( - this.matchRepo, - this.playerRepo, - this.protocolRegistry, - this.mockGameService, - this.mockNotificationService, - this.eventBus - ); - - this.queueService = new QueueService( - this.queueRepo, - this.matchService, - this.eventBus, - { minQueueTime: 0, protocol: "head-to-head" } - ); - - this.eventBus.subscribeAll((event) => { - this.events.push(event); - }); - } - - createPlayer(overrides?: Partial): Player { - const id = this.playerCounter++; - const player = createPlayer({ - discordId: `discord-${id}`, - osuId: 100000 + id, - osuName: `Player${id}`, - displayName: `Player ${id}`, - rating: 1000, - ...overrides, - }); - - this.playerRepo.save(player); - return player; - } - - createPlayers(count: number, baseRating: number = 1000): Player[] { - const players: Player[] = []; - for (let i = 0; i < count; i++) { - players.push(this.createPlayer({ rating: baseRating })); - } - return players; - } - - createTeam(players: Player[], overrides?: Partial): Team { - const id = this.teamCounter++; - const team = createTeam({ - name: overrides?.name ?? `Team ${id}`, - players, - ...overrides, - }); - - this.teamRepo.save(team); - return team; - } - - async simulateAllPlayersJoin(match: Match): Promise { - for (const team of match.teams) { - for (const player of team.players) { - if (match.lobbyId) { - this.mockGameService.simulatePlayerJoin(match.lobbyId, player.osuId); - } - await this.matchService.handlePlayerJoined(match.id, player); - } - } - } - - async simulateRolls( - match: Match, - rolls: Map - ): Promise { - for (const team of match.teams) { - const roll = rolls.get(team.id) ?? Math.floor(Math.random() * 100) + 1; - const captain = team.captain; - if (match.lobbyId) { - this.mockGameService.simulateRoll(match.lobbyId, captain.osuId, roll); - } - await this.matchService.handleRoll(match.id, captain, roll); - } - } - - async simulateMapComplete(match: Match, winningTeamId: string): Promise { - const scores: Score[] = []; - const winningTeam = match.teams.find((t) => t.id === winningTeamId)!; - const losingTeam = match.teams.find((t) => t.id !== winningTeamId)!; - - for (const player of winningTeam.players) { - scores.push({ - playerId: player.id, - score: 500000 + Math.floor(Math.random() * 100000), - accuracy: 95 + Math.random() * 5, - maxCombo: 800 + Math.floor(Math.random() * 200), - misses: Math.floor(Math.random() * 5), - }); - } - - for (const player of losingTeam.players) { - scores.push({ - playerId: player.id, - score: 400000 + Math.floor(Math.random() * 80000), - accuracy: 90 + Math.random() * 8, - maxCombo: 600 + Math.floor(Math.random() * 200), - misses: Math.floor(Math.random() * 10), - }); - } - - if (match.lobbyId) { - this.mockGameService.simulateMatchFinish(match.lobbyId, scores); - } - await this.matchService.handleMapComplete(match.id, scores); - } - - async runFullMatch( - team1: Team, - team2: Team, - protocolName: string = "head-to-head" - ): Promise { - await this.queueService.joinQueue(team1, "ranked"); - await this.queueService.joinQueue(team2, "ranked"); - - const match = await this.queueService.findMatch("ranked"); - if (!match) { - throw new Error("Failed to create match"); - } - - await this.simulateAllPlayersJoin(match); - - const rolls = new Map(); - rolls.set(team1.id, 75); - rolls.set(team2.id, 50); - await this.simulateRolls(match, rolls); - - const protocol = this.protocolRegistry.get(protocolName); - const winsNeeded = protocol.getWinsNeeded(); - - for (let i = 0; i < winsNeeded; i++) { - const updatedMatch = await this.matchRepo.findById(match.id); - if (!updatedMatch || updatedMatch.state === MatchState.COMPLETED) break; - - await this.simulateMapComplete(match, team1.id); - } - - return (await this.matchRepo.findById(match.id))!; - } - - getEventsByType(type: string): DomainEvent[] { - return this.events.filter((e) => e.type === type); - } - - clearEvents(): void { - this.events = []; - } - - reset(): void { - this.playerRepo.clear(); - this.teamRepo.clear(); - this.matchRepo.clear(); - this.queueRepo.clear(); - this.mockGameService.clear(); - this.mockNotificationService.clear(); - this.eventBus.clear(); - this.events = []; - this.playerCounter = 1; - this.teamCounter = 1; - } -} diff --git a/backend/src/testing/index.ts b/backend/src/testing/index.ts deleted file mode 100644 index 202c7a8..0000000 --- a/backend/src/testing/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./TestHarness"; diff --git a/backend/tests/application/QueueService.test.ts b/backend/tests/application/QueueService.test.ts deleted file mode 100644 index 4a1f27f..0000000 --- a/backend/tests/application/QueueService.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { test, expect, beforeEach } from "bun:test"; -import { TestHarness } from "../../src/testing/TestHarness"; - -let harness: TestHarness; - -beforeEach(() => { - harness = new TestHarness(); -}); - -test("team can join queue", async () => { - const players = harness.createPlayers(1); - const team = harness.createTeam(players, { name: "Test Team" }); - - const entry = await harness.queueService.joinQueue(team, "ranked"); - - expect(entry.team.id).toBe(team.id); - expect(entry.queueType).toBe("ranked"); -}); - -test("team cannot join queue twice", async () => { - const players = harness.createPlayers(1); - const team = harness.createTeam(players, { name: "Test Team" }); - - await harness.queueService.joinQueue(team, "ranked"); - - expect(async () => { - await harness.queueService.joinQueue(team, "ranked"); - }).toThrow(); -}); - -test("team can leave queue", async () => { - const players = harness.createPlayers(1); - const team = harness.createTeam(players, { name: "Test Team" }); - - await harness.queueService.joinQueue(team, "ranked"); - await harness.queueService.leaveQueue(team.id); - - const status = await harness.queueService.getQueueStatus("ranked"); - expect(status.count).toBe(0); -}); - -test("findMatch returns null when less than 2 teams in queue", async () => { - const players = harness.createPlayers(1); - const team = harness.createTeam(players, { name: "Test Team" }); - - await harness.queueService.joinQueue(team, "ranked"); - - const match = await harness.queueService.findMatch("ranked"); - expect(match).toBeNull(); -}); - -test("findMatch creates match when 2 teams in queue", async () => { - const players1 = harness.createPlayers(1, 1000); - const players2 = harness.createPlayers(1, 1010); - - const team1 = harness.createTeam(players1, { name: "Team 1" }); - const team2 = harness.createTeam(players2, { name: "Team 2" }); - - await harness.queueService.joinQueue(team1, "ranked"); - await harness.queueService.joinQueue(team2, "ranked"); - - const match = await harness.queueService.findMatch("ranked"); - - expect(match).not.toBeNull(); - expect(match!.teams.length).toBe(2); -}); - -test("getQueueStatus returns correct count", async () => { - const players1 = harness.createPlayers(1); - const players2 = harness.createPlayers(1); - - const team1 = harness.createTeam(players1, { name: "Team 1" }); - const team2 = harness.createTeam(players2, { name: "Team 2" }); - - await harness.queueService.joinQueue(team1, "ranked"); - await harness.queueService.joinQueue(team2, "ranked"); - - const status = await harness.queueService.getQueueStatus("ranked"); - - expect(status.count).toBe(2); - expect(status.entries.length).toBe(2); -}); - -test("queue events are published", async () => { - const players = harness.createPlayers(1); - const team = harness.createTeam(players, { name: "Test Team" }); - - await harness.queueService.joinQueue(team, "ranked"); - - const joinEvents = harness.getEventsByType("queue.team_joined"); - expect(joinEvents.length).toBe(1); - - await harness.queueService.leaveQueue(team.id); - - const leaveEvents = harness.getEventsByType("queue.team_left"); - expect(leaveEvents.length).toBe(1); -}); diff --git a/backend/tests/domain/protocols/StandardOsuProtocol.test.ts b/backend/tests/domain/protocols/StandardOsuProtocol.test.ts deleted file mode 100644 index 64a883b..0000000 --- a/backend/tests/domain/protocols/StandardOsuProtocol.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { test, expect, beforeEach } from "bun:test"; -import { StandardOsuProtocol } from "../../../src/domain/protocols/StandardOsuProtocol"; -import { createMatch } from "../../../src/domain/models/Match"; -import { createTeam } from "../../../src/domain/models/Team"; -import { createPlayer } from "../../../src/domain/models/Player"; -import type { Score } from "../../../src/domain/models/Score"; - -let protocol: StandardOsuProtocol; - -beforeEach(() => { - protocol = new StandardOsuProtocol(); -}); - -test("has correct configuration", () => { - expect(protocol.name).toBe("standard-osu"); - expect(protocol.teamSize).toBe(4); - expect(protocol.bestOf).toBe(9); - expect(protocol.hasPickBan).toBe(true); - expect(protocol.hasFreemod).toBe(true); -}); - -test("getWinsNeeded returns correct value for best of 9", () => { - expect(protocol.getWinsNeeded()).toBe(5); -}); - -test("calculateMapWinner returns winning team based on scores", () => { - const players1 = [ - createPlayer({ id: "p1", discordId: "d1", osuId: 1, osuName: "Player1" }), - createPlayer({ id: "p2", discordId: "d2", osuId: 2, osuName: "Player2" }), - ]; - const players2 = [ - createPlayer({ id: "p3", discordId: "d3", osuId: 3, osuName: "Player3" }), - createPlayer({ id: "p4", discordId: "d4", osuId: 4, osuName: "Player4" }), - ]; - - const team1 = createTeam({ id: "team1", name: "Team 1", players: players1 }); - const team2 = createTeam({ id: "team2", name: "Team 2", players: players2 }); - - const match = createMatch({ - teams: [team1, team2], - protocol: "standard-osu", - }); - - const scores: Score[] = [ - { playerId: "p1", score: 500000, accuracy: 95, maxCombo: 800, misses: 2 }, - { playerId: "p2", score: 450000, accuracy: 94, maxCombo: 750, misses: 3 }, - { playerId: "p3", score: 400000, accuracy: 92, maxCombo: 700, misses: 5 }, - { playerId: "p4", score: 380000, accuracy: 91, maxCombo: 680, misses: 6 }, - ]; - - const winner = protocol.calculateMapWinner(match, scores); - expect(winner).toBe("team1"); -}); - -test("calculateMatchWinner returns null when no team has enough wins", () => { - const players1 = [createPlayer({ id: "p1", discordId: "d1", osuId: 1, osuName: "P1" })]; - const players2 = [createPlayer({ id: "p2", discordId: "d2", osuId: 2, osuName: "P2" })]; - - const team1 = createTeam({ id: "team1", name: "Team 1", players: players1 }); - const team2 = createTeam({ id: "team2", name: "Team 2", players: players2 }); - - const match = createMatch({ - teams: [team1, team2], - protocol: "standard-osu", - }); - - expect(protocol.calculateMatchWinner(match)).toBeNull(); -}); - -test("isValidPick returns false when not the picking team", () => { - const players1 = [createPlayer({ id: "p1", discordId: "d1", osuId: 1, osuName: "P1" })]; - const players2 = [createPlayer({ id: "p2", discordId: "d2", osuId: 2, osuName: "P2" })]; - - const team1 = createTeam({ id: "team1", name: "Team 1", players: players1 }); - const team2 = createTeam({ id: "team2", name: "Team 2", players: players2 }); - - const match = createMatch({ - teams: [team1, team2], - protocol: "standard-osu", - }); - - match.currentPicker = "team1"; - - const map = { - id: 123, - beatmapSetId: 456, - title: "Test Map", - artist: "Artist", - difficulty: "Hard", - starRating: 5.5, - bpm: 180, - length: 120, - }; - - expect(protocol.isValidPick(match, team1, map)).toBe(true); - expect(protocol.isValidPick(match, team2, map)).toBe(false); -}); - -test("isValidPick returns false for banned maps", () => { - const players1 = [createPlayer({ id: "p1", discordId: "d1", osuId: 1, osuName: "P1" })]; - const players2 = [createPlayer({ id: "p2", discordId: "d2", osuId: 2, osuName: "P2" })]; - - const team1 = createTeam({ id: "team1", name: "Team 1", players: players1 }); - const team2 = createTeam({ id: "team2", name: "Team 2", players: players2 }); - - const match = createMatch({ - teams: [team1, team2], - protocol: "standard-osu", - }); - - match.currentPicker = "team1"; - match.bans.set("team2", [123]); - - const map = { - id: 123, - beatmapSetId: 456, - title: "Test Map", - artist: "Artist", - difficulty: "Hard", - starRating: 5.5, - bpm: 180, - length: 120, - }; - - expect(protocol.isValidPick(match, team1, map)).toBe(false); -}); - -test("isValidPick returns false for already picked maps", () => { - const players1 = [createPlayer({ id: "p1", discordId: "d1", osuId: 1, osuName: "P1" })]; - const players2 = [createPlayer({ id: "p2", discordId: "d2", osuId: 2, osuName: "P2" })]; - - const team1 = createTeam({ id: "team1", name: "Team 1", players: players1 }); - const team2 = createTeam({ id: "team2", name: "Team 2", players: players2 }); - - const match = createMatch({ - teams: [team1, team2], - protocol: "standard-osu", - }); - - match.currentPicker = "team1"; - match.picks.push(123); - - const map = { - id: 123, - beatmapSetId: 456, - title: "Test Map", - artist: "Artist", - difficulty: "Hard", - starRating: 5.5, - bpm: 180, - length: 120, - }; - - expect(protocol.isValidPick(match, team1, map)).toBe(false); -}); diff --git a/backend/tests/integration/full-match-flow.test.ts b/backend/tests/integration/full-match-flow.test.ts deleted file mode 100644 index 2c145f9..0000000 --- a/backend/tests/integration/full-match-flow.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { test, expect, beforeEach } from "bun:test"; -import { TestHarness } from "../../src/testing/TestHarness"; -import { MatchState } from "../../src/domain/models/MatchState"; - -let harness: TestHarness; - -beforeEach(() => { - harness = new TestHarness(); -}); - -test("complete match flow from queue to completion", async () => { - const players1 = harness.createPlayers(1, 1000); - const players2 = harness.createPlayers(1, 1020); - - const team1 = harness.createTeam(players1, { name: "Team Alpha" }); - const team2 = harness.createTeam(players2, { name: "Team Beta" }); - - await harness.queueService.joinQueue(team1, "ranked"); - await harness.queueService.joinQueue(team2, "ranked"); - - const match = await harness.queueService.findMatch("ranked"); - - expect(match).not.toBeNull(); - expect(match!.teams.length).toBe(2); - expect(match!.lobbyId).toBeDefined(); - - await harness.simulateAllPlayersJoin(match!); - - let updatedMatch = await harness.matchRepo.findById(match!.id); - expect(updatedMatch!.state).toBe(MatchState.ROLLING); - - const rolls = new Map(); - rolls.set(team1.id, 75); - rolls.set(team2.id, 50); - await harness.simulateRolls(match!, rolls); - - updatedMatch = await harness.matchRepo.findById(match!.id); - expect(updatedMatch!.state).toBe(MatchState.PICKING); - - const events = harness.getEventsByType("match.created"); - expect(events.length).toBe(1); - - const stateChanges = harness.getEventsByType("match.state_changed"); - expect(stateChanges.length).toBeGreaterThan(0); -}); - -test("match creates lobby on start", async () => { - const players1 = harness.createPlayers(1); - const players2 = harness.createPlayers(1); - - const team1 = harness.createTeam(players1, { name: "Team 1" }); - const team2 = harness.createTeam(players2, { name: "Team 2" }); - - await harness.queueService.joinQueue(team1, "ranked"); - await harness.queueService.joinQueue(team2, "ranked"); - - const match = await harness.queueService.findMatch("ranked"); - - expect(match!.lobbyId).toBeDefined(); - expect(harness.mockGameService.lobbies.has(match!.lobbyId!)).toBe(true); -}); - -test("notifications are sent when match is created", async () => { - const players1 = harness.createPlayers(1); - const players2 = harness.createPlayers(1); - - const team1 = harness.createTeam(players1, { name: "Team 1" }); - const team2 = harness.createTeam(players2, { name: "Team 2" }); - - await harness.queueService.joinQueue(team1, "ranked"); - await harness.queueService.joinQueue(team2, "ranked"); - - await harness.queueService.findMatch("ranked"); - - expect(harness.mockNotificationService.announcements.length).toBe(1); -}); - -test("match state transitions correctly through rolling phase", async () => { - const players1 = harness.createPlayers(1); - const players2 = harness.createPlayers(1); - - const team1 = harness.createTeam(players1, { name: "Team 1" }); - const team2 = harness.createTeam(players2, { name: "Team 2" }); - - await harness.queueService.joinQueue(team1, "ranked"); - await harness.queueService.joinQueue(team2, "ranked"); - - const match = await harness.queueService.findMatch("ranked"); - expect(match!.state).toBe(MatchState.WAITING_PLAYERS); - - await harness.simulateAllPlayersJoin(match!); - - let updatedMatch = await harness.matchRepo.findById(match!.id); - expect(updatedMatch!.state).toBe(MatchState.ROLLING); - - const rolls = new Map(); - rolls.set(team1.id, 75); - rolls.set(team2.id, 50); - await harness.simulateRolls(match!, rolls); - - updatedMatch = await harness.matchRepo.findById(match!.id); - expect(updatedMatch!.state).toBe(MatchState.PICKING); - // In head-to-head protocol, roll winner picks first - expect(updatedMatch!.currentPicker).toBe(team1.id); -}); - -test("queue status is empty after match found", async () => { - const players1 = harness.createPlayers(1); - const players2 = harness.createPlayers(1); - - const team1 = harness.createTeam(players1, { name: "Team 1" }); - const team2 = harness.createTeam(players2, { name: "Team 2" }); - - await harness.queueService.joinQueue(team1, "ranked"); - await harness.queueService.joinQueue(team2, "ranked"); - - await harness.queueService.findMatch("ranked"); - - const status = await harness.queueService.getQueueStatus("ranked"); - expect(status.count).toBe(0); -}); diff --git a/backend/tsconfig.json b/backend/tsconfig.json deleted file mode 100644 index bfa0fea..0000000 --- a/backend/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "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 - } -} diff --git a/frontend/.env.example b/frontend/.env.example deleted file mode 100644 index ec707ff..0000000 --- a/frontend/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -# Replace with your DB credentials! -DATABASE_URL="postgres://user:password@host:port/db-name" diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index bff793d..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -test-results -node_modules - -# Output -.output -.vercel -.netlify -.wrangler -/.svelte-kit -/build - -# OS -.DS_Store -Thumbs.db - -# Env -.env -.env.* -!.env.example -!.env.test - -# Vite -vite.config.js.timestamp-* -vite.config.ts.timestamp-* diff --git a/frontend/.npmrc b/frontend/.npmrc deleted file mode 100644 index b6f27f1..0000000 --- a/frontend/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/frontend/.prettierignore b/frontend/.prettierignore deleted file mode 100644 index 0243576..0000000 --- a/frontend/.prettierignore +++ /dev/null @@ -1,10 +0,0 @@ -# Package Managers -package-lock.json -pnpm-lock.yaml -yarn.lock -bun.lock -bun.lockb - -# Miscellaneous -/static/ -/drizzle/ diff --git a/frontend/.prettierrc b/frontend/.prettierrc deleted file mode 100644 index 819fa57..0000000 --- a/frontend/.prettierrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "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/frontend/.vscode/settings.json b/frontend/.vscode/settings.json deleted file mode 100644 index bc31e15..0000000 --- a/frontend/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "files.associations": { - "*.css": "tailwindcss" - } -} diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 10af74d..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# 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,forms" sveltekit-adapter="adapter:node" devtools-json drizzle="database:postgresql+postgresql:postgres.js+docker:no" lucia="demo:yes" --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/frontend/bun.lock b/frontend/bun.lock deleted file mode 100644 index 11cd1e6..0000000 --- a/frontend/bun.lock +++ /dev/null @@ -1,854 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "frontend", - "dependencies": { - "@node-rs/argon2": "^2.0.2", - "postgres": "^3.4.7", - }, - "devDependencies": { - "@eslint/compat": "^1.4.0", - "@eslint/js": "^9.39.1", - "@oslojs/crypto": "^1.0.1", - "@oslojs/encoding": "^1.1.0", - "@playwright/test": "^1.57.0", - "@sveltejs/adapter-node": "^5.4.0", - "@sveltejs/kit": "^2.49.1", - "@sveltejs/vite-plugin-svelte": "^6.2.1", - "@tailwindcss/forms": "^0.5.10", - "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.1.17", - "@types/node": "^22", - "@vitest/browser-playwright": "^4.0.15", - "drizzle-kit": "^0.31.8", - "drizzle-orm": "^0.45.0", - "eslint": "^9.39.1", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-svelte": "^3.13.1", - "globals": "^16.5.0", - "playwright": "^1.57.0", - "prettier": "^3.7.4", - "prettier-plugin-svelte": "^3.4.0", - "prettier-plugin-tailwindcss": "^0.7.2", - "svelte": "^5.45.6", - "svelte-check": "^4.3.4", - "tailwindcss": "^4.1.17", - "typescript": "^5.9.3", - "typescript-eslint": "^8.48.1", - "vite": "^7.2.6", - "vite-plugin-devtools-json": "^1.0.0", - "vitest": "^4.0.15", - "vitest-browser-svelte": "^2.0.1", - }, - }, - }, - "packages": { - "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], - - "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - - "@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@1.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0" }, "peerDependencies": { "eslint": "^8.40 || 9" }, "optionalPeers": ["eslint"] }, "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w=="], - - "@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@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "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.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], - - "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], - - "@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=="], - - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], - - "@node-rs/argon2": ["@node-rs/argon2@2.0.2", "", { "optionalDependencies": { "@node-rs/argon2-android-arm-eabi": "2.0.2", "@node-rs/argon2-android-arm64": "2.0.2", "@node-rs/argon2-darwin-arm64": "2.0.2", "@node-rs/argon2-darwin-x64": "2.0.2", "@node-rs/argon2-freebsd-x64": "2.0.2", "@node-rs/argon2-linux-arm-gnueabihf": "2.0.2", "@node-rs/argon2-linux-arm64-gnu": "2.0.2", "@node-rs/argon2-linux-arm64-musl": "2.0.2", "@node-rs/argon2-linux-x64-gnu": "2.0.2", "@node-rs/argon2-linux-x64-musl": "2.0.2", "@node-rs/argon2-wasm32-wasi": "2.0.2", "@node-rs/argon2-win32-arm64-msvc": "2.0.2", "@node-rs/argon2-win32-ia32-msvc": "2.0.2", "@node-rs/argon2-win32-x64-msvc": "2.0.2" } }, "sha512-t64wIsPEtNd4aUPuTAyeL2ubxATCBGmeluaKXEMAFk/8w6AJIVVkeLKMBpgLW6LU2t5cQxT+env/c6jxbtTQBg=="], - - "@node-rs/argon2-android-arm-eabi": ["@node-rs/argon2-android-arm-eabi@2.0.2", "", { "os": "android", "cpu": "arm" }, "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw=="], - - "@node-rs/argon2-android-arm64": ["@node-rs/argon2-android-arm64@2.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg=="], - - "@node-rs/argon2-darwin-arm64": ["@node-rs/argon2-darwin-arm64@2.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3TTNL/7wbcpNju5YcqUrCgXnXUSbD7ogeAKatzBVHsbpjZQbNb1NDxDjqqrWoTt6XL3z9mJUMGwbAk7zQltHtA=="], - - "@node-rs/argon2-darwin-x64": ["@node-rs/argon2-darwin-x64@2.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw=="], - - "@node-rs/argon2-freebsd-x64": ["@node-rs/argon2-freebsd-x64@2.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg=="], - - "@node-rs/argon2-linux-arm-gnueabihf": ["@node-rs/argon2-linux-arm-gnueabihf@2.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww=="], - - "@node-rs/argon2-linux-arm64-gnu": ["@node-rs/argon2-linux-arm64-gnu@2.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew=="], - - "@node-rs/argon2-linux-arm64-musl": ["@node-rs/argon2-linux-arm64-musl@2.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA=="], - - "@node-rs/argon2-linux-x64-gnu": ["@node-rs/argon2-linux-x64-gnu@2.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ZM3jrHuJ0dKOhvA80gKJqBpBRmTJTFSo2+xVZR+phQcbAKRlDMSZMFDiKbSTnctkfwNFtjgDdh5g1vaEV04AvA=="], - - "@node-rs/argon2-linux-x64-musl": ["@node-rs/argon2-linux-x64-musl@2.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw=="], - - "@node-rs/argon2-wasm32-wasi": ["@node-rs/argon2-wasm32-wasi@2.0.2", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.5" }, "cpu": "none" }, "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ=="], - - "@node-rs/argon2-win32-arm64-msvc": ["@node-rs/argon2-win32-arm64-msvc@2.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ=="], - - "@node-rs/argon2-win32-ia32-msvc": ["@node-rs/argon2-win32-ia32-msvc@2.0.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ=="], - - "@node-rs/argon2-win32-x64-msvc": ["@node-rs/argon2-win32-x64-msvc@2.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw=="], - - "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], - - "@oslojs/binary": ["@oslojs/binary@1.0.0", "", {}, "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ=="], - - "@oslojs/crypto": ["@oslojs/crypto@1.0.1", "", { "dependencies": { "@oslojs/asn1": "1.0.0", "@oslojs/binary": "1.0.0" } }, "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ=="], - - "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], - - "@playwright/test": ["@playwright/test@1.58.0", "", { "dependencies": { "playwright": "1.58.0" }, "bin": { "playwright": "cli.js" } }, "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg=="], - - "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], - - "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.9", "", { "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-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA=="], - - "@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.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.56.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg=="], - - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw=="], - - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA=="], - - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.56.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="], - - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="], - - "@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.5.2", "", { "dependencies": { "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "rollup": "^4.9.5" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-L15Djwpr7HrSAPj/Z8PYfc0pa9A1tllrr18phKI0WJHJeoWw45yinPf0IGgVTmakqx1B3JQ+C/OFl9ZwmxHU1Q=="], - - "@sveltejs/kit": ["@sveltejs/kit@2.50.1", "", { "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.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.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", "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" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-XRHD2i3zC4ukhz2iCQzO4mbsts081PAZnnMAQ7LNpWeYgeBmwMsalf0FGSwhFXBbtr2XViPKnFJBDCckWqrsLw=="], - - "@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/forms": ["@tailwindcss/forms@0.5.11", "", { "dependencies": { "mini-svg-data-uri": "^1.2.3" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" } }, "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA=="], - - "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], - - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], - - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="], - - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="], - - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="], - - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="], - - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="], - - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="], - - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="], - - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="], - - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="], - - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="], - - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="], - - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="], - - "@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.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], - - "@testing-library/svelte-core": ["@testing-library/svelte-core@1.0.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0" } }, "sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ=="], - - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - - "@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.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], - - "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], - - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/type-utils": "8.53.1", "@typescript-eslint/utils": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg=="], - - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.53.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.1", "@typescript-eslint/types": "^8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog=="], - - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1" } }, "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ=="], - - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.53.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA=="], - - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/utils": "8.53.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w=="], - - "@typescript-eslint/types": ["@typescript-eslint/types@8.53.1", "", {}, "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A=="], - - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.53.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.53.1", "@typescript-eslint/tsconfig-utils": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg=="], - - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg=="], - - "@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.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "ajv": ["ajv@6.12.6", "", { "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-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - - "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.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - - "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=="], - - "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=="], - - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "devalue": ["devalue@5.6.2", "", {}, "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg=="], - - "drizzle-kit": ["drizzle-kit@0.31.8", "", { "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-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg=="], - - "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.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], - - "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.2", "", { "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.2", "@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-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], - - "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.14.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", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-Isw0GvaMm0yHxAj71edAdGFh28ufYs+6rk2KlbbZphnqZAzrH3Se3t12IFh2H9+1F/jlDhBBL4oiOJmLqmYX0g=="], - - "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.2", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ=="], - - "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.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], - - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], - - "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=="], - - "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=="], - - "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.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], - - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], - - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], - - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], - - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], - - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], - - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], - - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], - - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], - - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], - - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], - - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], - - "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], - - "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=="], - - "mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": { "mini-svg-data-uri": "cli.js" } }, "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="], - - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "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=="], - - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - - "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.0", "", { "dependencies": { "playwright-core": "1.58.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ=="], - - "playwright-core": ["playwright-core@1.58.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw=="], - - "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.4.1", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg=="], - - "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=="], - - "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.56.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.56.0", "@rollup/rollup-android-arm64": "4.56.0", "@rollup/rollup-darwin-arm64": "4.56.0", "@rollup/rollup-darwin-x64": "4.56.0", "@rollup/rollup-freebsd-arm64": "4.56.0", "@rollup/rollup-freebsd-x64": "4.56.0", "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", "@rollup/rollup-linux-arm-musleabihf": "4.56.0", "@rollup/rollup-linux-arm64-gnu": "4.56.0", "@rollup/rollup-linux-arm64-musl": "4.56.0", "@rollup/rollup-linux-loong64-gnu": "4.56.0", "@rollup/rollup-linux-loong64-musl": "4.56.0", "@rollup/rollup-linux-ppc64-gnu": "4.56.0", "@rollup/rollup-linux-ppc64-musl": "4.56.0", "@rollup/rollup-linux-riscv64-gnu": "4.56.0", "@rollup/rollup-linux-riscv64-musl": "4.56.0", "@rollup/rollup-linux-s390x-gnu": "4.56.0", "@rollup/rollup-linux-x64-gnu": "4.56.0", "@rollup/rollup-linux-x64-musl": "4.56.0", "@rollup/rollup-openbsd-x64": "4.56.0", "@rollup/rollup-openharmony-arm64": "4.56.0", "@rollup/rollup-win32-arm64-msvc": "4.56.0", "@rollup/rollup-win32-ia32-msvc": "4.56.0", "@rollup/rollup-win32-x64-gnu": "4.56.0", "@rollup/rollup-win32-x64-msvc": "4.56.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg=="], - - "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], - - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], - - "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.48.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-+NUe82VoFP1RQViZI/esojx70eazGF4u0O/9ucqZ4rPcOZD+n5EVp17uYsqwdzjUjZyTpGKunHbDziW6AIAVkQ=="], - - "svelte-check": ["svelte-check@4.3.5", "", { "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-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q=="], - - "svelte-eslint-parser": ["svelte-eslint-parser@1.4.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" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA=="], - - "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], - - "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=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "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=="], - - "typescript-eslint": ["typescript-eslint@8.53.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.1", "@typescript-eslint/parser": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/utils": "8.53.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg=="], - - "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.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], - - "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=="], - - "@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/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - - "@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@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "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.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], - - "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@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], - - "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], - - "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], - - "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], - - "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], - - "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], - - "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], - - "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], - - "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], - - "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], - - "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], - - "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], - - "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], - - "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], - - "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], - - "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], - - "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], - - "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], - - "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], - - "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], - - "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], - - "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], - - "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], - - "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], - - "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], - - "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], - } -} diff --git a/frontend/drizzle.config.ts b/frontend/drizzle.config.ts deleted file mode 100644 index d57e081..0000000 --- a/frontend/drizzle.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -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/frontend/e2e/demo.test.ts b/frontend/e2e/demo.test.ts deleted file mode 100644 index 9985ce1..0000000 --- a/frontend/e2e/demo.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -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/frontend/eslint.config.js b/frontend/eslint.config.js deleted file mode 100644 index 7df665d..0000000 --- a/frontend/eslint.config.js +++ /dev/null @@ -1,41 +0,0 @@ -import prettier from 'eslint-config-prettier'; -import { fileURLToPath } from 'node:url'; -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 = fileURLToPath(new URL('./.gitignore', import.meta.url)); - -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/frontend/package.json b/frontend/package.json deleted file mode 100644 index 79cdc92..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "frontend", - "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" - }, - "devDependencies": { - "@eslint/compat": "^1.4.0", - "@eslint/js": "^9.39.1", - "@oslojs/crypto": "^1.0.1", - "@oslojs/encoding": "^1.1.0", - "@playwright/test": "^1.57.0", - "@sveltejs/adapter-node": "^5.4.0", - "@sveltejs/kit": "^2.49.1", - "@sveltejs/vite-plugin-svelte": "^6.2.1", - "@tailwindcss/forms": "^0.5.10", - "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.1.17", - "@types/node": "^22", - "@vitest/browser-playwright": "^4.0.15", - "drizzle-kit": "^0.31.8", - "drizzle-orm": "^0.45.0", - "eslint": "^9.39.1", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-svelte": "^3.13.1", - "globals": "^16.5.0", - "playwright": "^1.57.0", - "prettier": "^3.7.4", - "prettier-plugin-svelte": "^3.4.0", - "prettier-plugin-tailwindcss": "^0.7.2", - "svelte": "^5.45.6", - "svelte-check": "^4.3.4", - "tailwindcss": "^4.1.17", - "typescript": "^5.9.3", - "typescript-eslint": "^8.48.1", - "vite": "^7.2.6", - "vite-plugin-devtools-json": "^1.0.0", - "vitest": "^4.0.15", - "vitest-browser-svelte": "^2.0.1" - }, - "dependencies": { - "@node-rs/argon2": "^2.0.2", - "postgres": "^3.4.7" - } -} diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts deleted file mode 100644 index 8f5062c..0000000 --- a/frontend/playwright.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from '@playwright/test'; - -export default defineConfig({ - webServer: { command: 'npm run build && npm run preview', port: 4173 }, - testDir: 'e2e' -}); diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts deleted file mode 100644 index fc79747..0000000 --- a/frontend/src/app.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces -declare global { - namespace App { - interface Locals { - user: import('$lib/server/auth').SessionValidationResult['user']; - session: import('$lib/server/auth').SessionValidationResult['session']; - } - - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } -} - -export {}; diff --git a/frontend/src/app.html b/frontend/src/app.html deleted file mode 100644 index f273cc5..0000000 --- a/frontend/src/app.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/frontend/src/demo.spec.ts b/frontend/src/demo.spec.ts deleted file mode 100644 index e07cbbd..0000000 --- a/frontend/src/demo.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -describe('sum test', () => { - it('adds 1 + 2 to equal 3', () => { - expect(1 + 2).toBe(3); - }); -}); diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts deleted file mode 100644 index 3c37e24..0000000 --- a/frontend/src/hooks.server.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Handle } from '@sveltejs/kit'; -import * as auth from '$lib/server/auth'; - -const handleAuth: Handle = async ({ event, resolve }) => { - const sessionToken = event.cookies.get(auth.sessionCookieName); - - if (!sessionToken) { - event.locals.user = null; - event.locals.session = null; - - return resolve(event); - } - - const { session, user } = await auth.validateSessionToken(sessionToken); - - if (session) { - auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); - } else { - auth.deleteSessionTokenCookie(event); - } - - event.locals.user = user; - event.locals.session = session; - - return resolve(event); -}; - -export const handle: Handle = handleAuth; diff --git a/frontend/src/lib/assets/favicon.svg b/frontend/src/lib/assets/favicon.svg deleted file mode 100644 index cc5dc66..0000000 --- a/frontend/src/lib/assets/favicon.svg +++ /dev/null @@ -1 +0,0 @@ -svelte-logo \ No newline at end of file diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts deleted file mode 100644 index 856f2b6..0000000 --- a/frontend/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/frontend/src/lib/server/auth.ts b/frontend/src/lib/server/auth.ts deleted file mode 100644 index 38c9930..0000000 --- a/frontend/src/lib/server/auth.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { RequestEvent } from '@sveltejs/kit'; -import { eq } from 'drizzle-orm'; -import { sha256 } from '@oslojs/crypto/sha2'; -import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding'; -import { db } from '$lib/server/db'; -import * as table from '$lib/server/db/schema'; - -const DAY_IN_MS = 1000 * 60 * 60 * 24; - -export const sessionCookieName = 'auth-session'; - -export function generateSessionToken() { - const bytes = crypto.getRandomValues(new Uint8Array(18)); - const token = encodeBase64url(bytes); - return token; -} - -export async function createSession(token: string, userId: string) { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const session: table.Session = { - id: sessionId, - userId, - expiresAt: new Date(Date.now() + DAY_IN_MS * 30) - }; - await db.insert(table.session).values(session); - return session; -} - -export async function validateSessionToken(token: string) { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const [result] = await db - .select({ - // Adjust user table here to tweak returned data - user: { id: table.user.id, username: table.user.username }, - session: table.session - }) - .from(table.session) - .innerJoin(table.user, eq(table.session.userId, table.user.id)) - .where(eq(table.session.id, sessionId)); - - if (!result) { - return { session: null, user: null }; - } - const { session, user } = result; - - const sessionExpired = Date.now() >= session.expiresAt.getTime(); - if (sessionExpired) { - await db.delete(table.session).where(eq(table.session.id, session.id)); - return { session: null, user: null }; - } - - const renewSession = Date.now() >= session.expiresAt.getTime() - DAY_IN_MS * 15; - if (renewSession) { - session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30); - await db - .update(table.session) - .set({ expiresAt: session.expiresAt }) - .where(eq(table.session.id, session.id)); - } - - return { session, user }; -} - -export type SessionValidationResult = Awaited>; - -export async function invalidateSession(sessionId: string) { - await db.delete(table.session).where(eq(table.session.id, sessionId)); -} - -export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date) { - event.cookies.set(sessionCookieName, token, { - expires: expiresAt, - path: '/' - }); -} - -export function deleteSessionTokenCookie(event: RequestEvent) { - event.cookies.delete(sessionCookieName, { - path: '/' - }); -} diff --git a/frontend/src/lib/server/db/index.ts b/frontend/src/lib/server/db/index.ts deleted file mode 100644 index fb8b355..0000000 --- a/frontend/src/lib/server/db/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -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/frontend/src/lib/server/db/schema.ts b/frontend/src/lib/server/db/schema.ts deleted file mode 100644 index cbb6d1a..0000000 --- a/frontend/src/lib/server/db/schema.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { pgTable, serial, integer, text, timestamp } from 'drizzle-orm/pg-core'; - -export const user = pgTable('user', { - id: text('id').primaryKey(), - age: integer('age'), - username: text('username').notNull().unique(), - passwordHash: text('password_hash').notNull() -}); - -export const session = pgTable('session', { - id: text('id').primaryKey(), - userId: text('user_id') - .notNull() - .references(() => user.id), - expiresAt: timestamp('expires_at', { withTimezone: true, mode: 'date' }).notNull() -}); - -export type Session = typeof session.$inferSelect; - -export type User = typeof user.$inferSelect; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte deleted file mode 100644 index 0d8eb03..0000000 --- a/frontend/src/routes/+layout.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - - -{@render children()} diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte deleted file mode 100644 index cc88df0..0000000 --- a/frontend/src/routes/+page.svelte +++ /dev/null @@ -1,2 +0,0 @@ -

Welcome to SvelteKit

-

Visit svelte.dev/docs/kit to read the documentation

diff --git a/frontend/src/routes/demo/+page.svelte b/frontend/src/routes/demo/+page.svelte deleted file mode 100644 index 501e687..0000000 --- a/frontend/src/routes/demo/+page.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - -lucia diff --git a/frontend/src/routes/demo/lucia/+page.server.ts b/frontend/src/routes/demo/lucia/+page.server.ts deleted file mode 100644 index 9fe83e5..0000000 --- a/frontend/src/routes/demo/lucia/+page.server.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as auth from '$lib/server/auth'; -import { fail, redirect } from '@sveltejs/kit'; -import { getRequestEvent } from '$app/server'; -import type { Actions, PageServerLoad } from './$types'; - -export const load: PageServerLoad = async () => { - const user = requireLogin(); - return { user }; -}; - -export const actions: Actions = { - logout: async (event) => { - if (!event.locals.session) { - return fail(401); - } - await auth.invalidateSession(event.locals.session.id); - auth.deleteSessionTokenCookie(event); - - return redirect(302, '/demo/lucia/login'); - } -}; - -function requireLogin() { - const { locals } = getRequestEvent(); - - if (!locals.user) { - return redirect(302, '/demo/lucia/login'); - } - - return locals.user; -} diff --git a/frontend/src/routes/demo/lucia/+page.svelte b/frontend/src/routes/demo/lucia/+page.svelte deleted file mode 100644 index cefb2d1..0000000 --- a/frontend/src/routes/demo/lucia/+page.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - -

Hi, {data.user.username}!

-

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

-
- -
diff --git a/frontend/src/routes/demo/lucia/login/+page.server.ts b/frontend/src/routes/demo/lucia/login/+page.server.ts deleted file mode 100644 index 97d1235..0000000 --- a/frontend/src/routes/demo/lucia/login/+page.server.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { hash, verify } from '@node-rs/argon2'; -import { encodeBase32LowerCase } from '@oslojs/encoding'; -import { fail, redirect } from '@sveltejs/kit'; -import { eq } from 'drizzle-orm'; -import * as auth from '$lib/server/auth'; -import { db } from '$lib/server/db'; -import * as table from '$lib/server/db/schema'; -import type { Actions, PageServerLoad } from './$types'; - -export const load: PageServerLoad = async (event) => { - if (event.locals.user) { - return redirect(302, '/demo/lucia'); - } - return {}; -}; - -export const actions: Actions = { - login: async (event) => { - const formData = await event.request.formData(); - const username = formData.get('username'); - const password = formData.get('password'); - - if (!validateUsername(username)) { - return fail(400, { - message: 'Invalid username (min 3, max 31 characters, alphanumeric only)' - }); - } - if (!validatePassword(password)) { - return fail(400, { message: 'Invalid password (min 6, max 255 characters)' }); - } - - const results = await db.select().from(table.user).where(eq(table.user.username, username)); - - const existingUser = results.at(0); - if (!existingUser) { - return fail(400, { message: 'Incorrect username or password' }); - } - - const validPassword = await verify(existingUser.passwordHash, password, { - memoryCost: 19456, - timeCost: 2, - outputLen: 32, - parallelism: 1 - }); - if (!validPassword) { - return fail(400, { message: 'Incorrect username or password' }); - } - - const sessionToken = auth.generateSessionToken(); - const session = await auth.createSession(sessionToken, existingUser.id); - auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); - - return redirect(302, '/demo/lucia'); - }, - register: async (event) => { - const formData = await event.request.formData(); - const username = formData.get('username'); - const password = formData.get('password'); - - if (!validateUsername(username)) { - return fail(400, { message: 'Invalid username' }); - } - if (!validatePassword(password)) { - return fail(400, { message: 'Invalid password' }); - } - - const userId = generateUserId(); - const passwordHash = await hash(password, { - // recommended minimum parameters - memoryCost: 19456, - timeCost: 2, - outputLen: 32, - parallelism: 1 - }); - - try { - await db.insert(table.user).values({ id: userId, username, passwordHash }); - - const sessionToken = auth.generateSessionToken(); - const session = await auth.createSession(sessionToken, userId); - auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); - } catch { - return fail(500, { message: 'An error has occurred' }); - } - return redirect(302, '/demo/lucia'); - } -}; - -function generateUserId() { - // ID with 120 bits of entropy, or about the same as UUID v4. - const bytes = crypto.getRandomValues(new Uint8Array(15)); - const id = encodeBase32LowerCase(bytes); - return id; -} - -function validateUsername(username: unknown): username is string { - return ( - typeof username === 'string' && - username.length >= 3 && - username.length <= 31 && - /^[a-z0-9_-]+$/.test(username) - ); -} - -function validatePassword(password: unknown): password is string { - return typeof password === 'string' && password.length >= 6 && password.length <= 255; -} diff --git a/frontend/src/routes/demo/lucia/login/+page.svelte b/frontend/src/routes/demo/lucia/login/+page.svelte deleted file mode 100644 index 39c8037..0000000 --- a/frontend/src/routes/demo/lucia/login/+page.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - -

Login/Register

-
- - - - -
-

{form?.message ?? ''}

diff --git a/frontend/src/routes/layout.css b/frontend/src/routes/layout.css deleted file mode 100644 index cd67023..0000000 --- a/frontend/src/routes/layout.css +++ /dev/null @@ -1,3 +0,0 @@ -@import 'tailwindcss'; -@plugin '@tailwindcss/forms'; -@plugin '@tailwindcss/typography'; diff --git a/frontend/src/routes/page.svelte.spec.ts b/frontend/src/routes/page.svelte.spec.ts deleted file mode 100644 index d54f00a..0000000 --- a/frontend/src/routes/page.svelte.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { page } from 'vitest/browser'; -import { describe, expect, it } from 'vitest'; -import { render } from 'vitest-browser-svelte'; -import Page from './+page.svelte'; - -describe('/+page.svelte', () => { - it('should render h1', async () => { - render(Page); - - const heading = page.getByRole('heading', { level: 1 }); - await expect.element(heading).toBeInTheDocument(); - }); -}); diff --git a/frontend/static/robots.txt b/frontend/static/robots.txt deleted file mode 100644 index b6dd667..0000000 --- a/frontend/static/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# allow crawling everything by default -User-agent: * -Disallow: diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js deleted file mode 100644 index e744595..0000000 --- a/frontend/svelte.config.js +++ /dev/null @@ -1,6 +0,0 @@ -import adapter from '@sveltejs/adapter-node'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { kit: { adapter: adapter() } }; - -export default config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json deleted file mode 100644 index 2c2ed3c..0000000 --- a/frontend/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "rewriteRelativeImportExtensions": true, - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" - } - // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias - // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files - // - // To make changes to top-level options such as include and exclude, we recommend extending - // the generated config; see https://svelte.dev/docs/kit/configuration#typescript -} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts deleted file mode 100644 index c4ffa6e..0000000 --- a/frontend/vite.config.ts +++ /dev/null @@ -1,43 +0,0 @@ -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}'] - } - } - ] - } -}); From 844fd590e1f297f22c9ca112673784d49cbd994a Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Fri, 27 Feb 2026 16:23:27 +0100 Subject: [PATCH 03/14] blank sveltekit project --- .env.example | 17 +- .gitignore | 25 +- .npmrc | 1 + .prettierignore | 10 + .prettierrc | 16 + .vscode/settings.json | 5 + README.md | 42 + bun.lock | 841 ++++++++++++++++++ compose.yml | 19 + drizzle.config.ts | 11 + e2e/demo.test.ts | 6 + eslint.config.js | 39 + package.json | 59 ++ playwright.config.ts | 6 + src/app.d.ts | 19 + src/app.html | 11 + src/demo.spec.ts | 7 + src/hooks.server.ts | 17 + src/lib/assets/favicon.svg | 1 + src/lib/index.ts | 1 + src/lib/server/auth.ts | 20 + src/lib/server/db/auth.schema.ts | 93 ++ src/lib/server/db/index.ts | 10 + src/lib/server/db/schema.ts | 9 + src/routes/+layout.svelte | 9 + src/routes/+page.svelte | 2 + src/routes/demo/+page.svelte | 5 + src/routes/demo/better-auth/+page.server.ts | 20 + src/routes/demo/better-auth/+page.svelte | 14 + .../demo/better-auth/login/+page.server.ts | 78 ++ .../demo/better-auth/login/+page.svelte | 51 ++ src/routes/layout.css | 2 + src/routes/page.svelte.spec.ts | 13 + static/robots.txt | 3 + svelte.config.js | 6 + tsconfig.json | 20 + vite.config.ts | 37 + 37 files changed, 1543 insertions(+), 2 deletions(-) create mode 100644 .npmrc create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 bun.lock create mode 100644 compose.yml create mode 100644 drizzle.config.ts create mode 100644 e2e/demo.test.ts create mode 100644 eslint.config.js create mode 100644 package.json create mode 100644 playwright.config.ts create mode 100644 src/app.d.ts create mode 100644 src/app.html create mode 100644 src/demo.spec.ts create mode 100644 src/hooks.server.ts create mode 100644 src/lib/assets/favicon.svg create mode 100644 src/lib/index.ts create mode 100644 src/lib/server/auth.ts create mode 100644 src/lib/server/db/auth.schema.ts create mode 100644 src/lib/server/db/index.ts create mode 100644 src/lib/server/db/schema.ts create mode 100644 src/routes/+layout.svelte create mode 100644 src/routes/+page.svelte create mode 100644 src/routes/demo/+page.svelte create mode 100644 src/routes/demo/better-auth/+page.server.ts create mode 100644 src/routes/demo/better-auth/+page.svelte create mode 100644 src/routes/demo/better-auth/login/+page.server.ts create mode 100644 src/routes/demo/better-auth/login/+page.svelte create mode 100644 src/routes/layout.css create mode 100644 src/routes/page.svelte.spec.ts create mode 100644 static/robots.txt create mode 100644 svelte.config.js create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.env.example b/.env.example index 2b29cb7..1b5373a 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,16 @@ -DB_URL="postgresql://user:password@db:5432/esports" \ No newline at end of file +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 a9c23ae..41f1bdc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,25 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env .env -.claude/ \ No newline at end of file +.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 new file mode 100644 index 0000000..4756f0c --- /dev/null +++ b/bun.lock @@ -0,0 +1,841 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "esports", + "dependencies": { + "postgres": "^3.4.8", + }, + "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": { + "@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=="], + + "@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=="], + + "@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=="], + + "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=="], + + "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=="], + + "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=="], + + "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=="], + + "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/package.json b/package.json new file mode 100644 index 0000000..abefed1 --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "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": { + "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..2ae6f13 --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,19 @@ +import type { User, Session } from 'better-auth/minimal'; + +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + interface Locals { + user?: User; + session?: Session; + } + + // interface Error {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..f273cc5 --- /dev/null +++ b/src/app.html @@ -0,0 +1,11 @@ + + + + + + %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..22e93f9 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,17 @@ +import type { Handle } from '@sveltejs/kit'; +import { building } from '$app/environment'; +import { auth } from '$lib/server/auth'; +import { svelteKitHandler } from 'better-auth/svelte-kit'; + +const handleBetterAuth: Handle = async ({ event, resolve }) => { + const session = await auth.api.getSession({ headers: event.request.headers }); + + if (session) { + event.locals.session = session.session; + event.locals.user = session.user; + } + + 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/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..af4affa --- /dev/null +++ b/src/lib/server/auth.ts @@ -0,0 +1,20 @@ +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'; + +export const auth = betterAuth({ + baseURL: env.ORIGIN, + secret: env.BETTER_AUTH_SECRET, + database: drizzleAdapter(db, { provider: 'pg' }), + emailAndPassword: { enabled: true }, + socialProviders: { + github: { + clientId: env.GITHUB_CLIENT_ID, + clientSecret: env.GITHUB_CLIENT_SECRET + } + }, + plugins: [sveltekitCookies(getRequestEvent)] // make sure this is the last plugin in the array +}); diff --git a/src/lib/server/db/auth.schema.ts b/src/lib/server/db/auth.schema.ts new file mode 100644 index 0000000..413a524 --- /dev/null +++ b/src/lib/server/db/auth.schema.ts @@ -0,0 +1,93 @@ +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"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ 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(() => /* @__PURE__ */ 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(() => /* @__PURE__ */ 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(() => /* @__PURE__ */ 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..5d1a8e0 --- /dev/null +++ b/src/lib/server/db/schema.ts @@ -0,0 +1,9 @@ +import { pgTable, serial, integer, text } from 'drizzle-orm/pg-core'; + +export const task = pgTable('task', { + id: serial('id').primaryKey(), + title: text('title').notNull(), + priority: integer('priority').notNull().default(1) +}); + +export * from './auth.schema'; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..0d8eb03 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,9 @@ + + + +{@render children()} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..cc88df0 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,2 @@ +

Welcome to SvelteKit

+

Visit svelte.dev/docs/kit to read the documentation

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..18ca5cb --- /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() ?? 'github'; + const callbackURL = formData.get('callbackURL')?.toString() ?? '/demo/better-auth'; + + const result = await auth.api.signInSocial({ + body: { + provider: provider as 'github', + 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..f7eb32a --- /dev/null +++ b/src/routes/demo/better-auth/login/+page.svelte @@ -0,0 +1,51 @@ + + +

Login

+
+ + + + + +
+

{form?.message ?? ''}

+ +
+
+ + + +
diff --git a/src/routes/layout.css b/src/routes/layout.css new file mode 100644 index 0000000..1c4d2a8 --- /dev/null +++ b/src/routes/layout.css @@ -0,0 +1,2 @@ +@import 'tailwindcss'; +@plugin '@tailwindcss/typography'; diff --git a/src/routes/page.svelte.spec.ts b/src/routes/page.svelte.spec.ts new file mode 100644 index 0000000..d54f00a --- /dev/null +++ b/src/routes/page.svelte.spec.ts @@ -0,0 +1,13 @@ +import { page } from 'vitest/browser'; +import { describe, expect, it } from 'vitest'; +import { render } from 'vitest-browser-svelte'; +import Page from './+page.svelte'; + +describe('/+page.svelte', () => { + it('should render h1', async () => { + render(Page); + + const heading = page.getByRole('heading', { level: 1 }); + await expect.element(heading).toBeInTheDocument(); + }); +}); 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/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 new file mode 100644 index 0000000..2c2ed3c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/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}'] + } + } + ] + } +}); From 0a6695fc2613ac29d5e6a201d95be45da3e08023 Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Sat, 28 Feb 2026 01:00:22 +0100 Subject: [PATCH 04/14] add basic db schema + auth + oauth with osu --- src/lib/server/auth.ts | 62 ++++++- src/lib/server/db/schema.ts | 164 +++++++++++++++++- .../demo/better-auth/login/+page.server.ts | 4 +- .../demo/better-auth/login/+page.svelte | 4 +- 4 files changed, 216 insertions(+), 18 deletions(-) diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index af4affa..6a7212d 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -4,17 +4,65 @@ 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'; export const auth = betterAuth({ baseURL: env.ORIGIN, secret: env.BETTER_AUTH_SECRET, database: drizzleAdapter(db, { provider: 'pg' }), emailAndPassword: { enabled: true }, - socialProviders: { - github: { - clientId: env.GITHUB_CLIENT_ID, - clientSecret: env.GITHUB_CLIENT_SECRET - } - }, - plugins: [sveltekitCookies(getRequestEvent)] // make sure this is the last plugin in the array + 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', + redirectURI: 'http://localhost:5173/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) // make sure this is the last plugin in the array + ] }); diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 5d1a8e0..0cb8087 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -1,9 +1,159 @@ -import { pgTable, serial, integer, text } from 'drizzle-orm/pg-core'; - -export const task = pgTable('task', { - id: serial('id').primaryKey(), - title: text('title').notNull(), - priority: integer('priority').notNull().default(1) -}); +import { + pgTable, + integer, + text, + uuid, + boolean, + timestamp, + jsonb, + index, + uniqueIndex, + real +} from 'drizzle-orm/pg-core'; 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(), + 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) => [ + uniqueIndex('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)] +); diff --git a/src/routes/demo/better-auth/login/+page.server.ts b/src/routes/demo/better-auth/login/+page.server.ts index 18ca5cb..9c25def 100644 --- a/src/routes/demo/better-auth/login/+page.server.ts +++ b/src/routes/demo/better-auth/login/+page.server.ts @@ -60,12 +60,12 @@ export const actions: Actions = { }, signInSocial: async (event) => { const formData = await event.request.formData(); - const provider = formData.get('provider')?.toString() ?? 'github'; + 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 'github', + provider: provider as 'osu', callbackURL } }); diff --git a/src/routes/demo/better-auth/login/+page.svelte b/src/routes/demo/better-auth/login/+page.svelte index f7eb32a..e7f8079 100644 --- a/src/routes/demo/better-auth/login/+page.svelte +++ b/src/routes/demo/better-auth/login/+page.svelte @@ -43,9 +43,9 @@
- + Sign in with osu!
From 191ec08f7ffa527d2349454ed629e7fd759a75eb Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Sat, 28 Feb 2026 01:08:29 +0100 Subject: [PATCH 05/14] create personal team on user creation --- src/lib/server/auth.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 6a7212d..7e82eaa 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -5,12 +5,36 @@ 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: [ From 290b9752a9877ccb015b93aedd949c0f75c3d59f Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Sat, 28 Feb 2026 01:17:52 +0100 Subject: [PATCH 06/14] add osu API --- src/lib/server/osu/api.ts | 119 +++++++++++++++++++++ src/routes/api/osu/beatmap/[id]/+server.ts | 14 +++ 2 files changed, 133 insertions(+) create mode 100644 src/lib/server/osu/api.ts create mode 100644 src/routes/api/osu/beatmap/[id]/+server.ts diff --git a/src/lib/server/osu/api.ts b/src/lib/server/osu/api.ts new file mode 100644 index 0000000..8104fb2 --- /dev/null +++ b/src/lib/server/osu/api.ts @@ -0,0 +1,119 @@ +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'; + +let clientToken: string | null = null; +let clientTokenExpiresAt = 0; + +// Client credentials token (no user needed, for public endpoints) +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!; +} + +// Get a user's access token (for user-scoped endpoints) +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; + + // Check if expired and refresh if needed + 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 ────────────────────────────────────────────────────────── + +export async function getBeatmap(beatmapId: string | number) { + return osuFetch(`/beatmaps/${beatmapId}`); +} + +export async function getBeatmapset(beatmapsetId: string | number) { + return osuFetch(`/beatmapsets/${beatmapsetId}`); +} + +export async function getUser(userId: string | number) { + return osuFetch(`/users/${userId}`); +} + +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/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'); + } +}; From 88fe116bcead1c64e410bc9f083781bf31d77aea Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Sat, 28 Feb 2026 01:22:35 +0100 Subject: [PATCH 07/14] mappool API --- src/lib/server/db/schema.ts | 53 +++++++++++++++++++ src/routes/api/mappools/+server.ts | 33 ++++++++++++ src/routes/api/mappools/[id]/+server.ts | 31 +++++++++++ src/routes/api/mappools/[id]/slots/+server.ts | 38 +++++++++++++ .../mappools/[id]/slots/[slotId]/+server.ts | 12 +++++ 5 files changed, 167 insertions(+) create mode 100644 src/routes/api/mappools/+server.ts create mode 100644 src/routes/api/mappools/[id]/+server.ts create mode 100644 src/routes/api/mappools/[id]/slots/+server.ts create mode 100644 src/routes/api/mappools/[id]/slots/[slotId]/+server.ts diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 0cb8087..6caa026 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -157,3 +157,56 @@ export const matchGameScore = pgTable( }, (t) => [index('idx_mgs_game').on(t.matchGameId)] ); + +import { relations } from 'drizzle-orm'; + +// ── 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] + }) +})); 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 }); +}; From 55f90e1a3d4633ba219141fe712c7ba35e2fb6a8 Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Sat, 28 Feb 2026 01:46:14 +0100 Subject: [PATCH 08/14] layout UI --- src/app.html | 6 ++++ src/lib/components/Header.svelte | 29 +++++++++++++++ src/lib/components/Sidebar.svelte | 57 ++++++++++++++++++++++++++++++ src/routes/(app)/+layout.server.ts | 7 ++++ src/routes/(app)/+layout.svelte | 16 +++++++++ src/routes/(app)/+page.svelte | 46 ++++++++++++++++++++++++ src/routes/(app)/layout.css | 43 ++++++++++++++++++++++ src/routes/+layout.svelte | 9 ----- src/routes/+page.svelte | 2 -- src/routes/layout.css | 2 -- src/routes/page.svelte.spec.ts | 13 ------- 11 files changed, 204 insertions(+), 26 deletions(-) create mode 100644 src/lib/components/Header.svelte create mode 100644 src/lib/components/Sidebar.svelte create mode 100644 src/routes/(app)/+layout.server.ts create mode 100644 src/routes/(app)/+layout.svelte create mode 100644 src/routes/(app)/+page.svelte create mode 100644 src/routes/(app)/layout.css delete mode 100644 src/routes/+layout.svelte delete mode 100644 src/routes/+page.svelte delete mode 100644 src/routes/layout.css delete mode 100644 src/routes/page.svelte.spec.ts diff --git a/src/app.html b/src/app.html index f273cc5..e0a3d41 100644 --- a/src/app.html +++ b/src/app.html @@ -3,6 +3,12 @@ + + + %sveltekit.head% diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte new file mode 100644 index 0000000..ed6a9f3 --- /dev/null +++ b/src/lib/components/Header.svelte @@ -0,0 +1,29 @@ + + +
+
+ +
+ {#if user} +
+ {#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..2712955 --- /dev/null +++ b/src/lib/components/Sidebar.svelte @@ -0,0 +1,57 @@ + + + diff --git a/src/routes/(app)/+layout.server.ts b/src/routes/(app)/+layout.server.ts new file mode 100644 index 0000000..c9d39ee --- /dev/null +++ b/src/routes/(app)/+layout.server.ts @@ -0,0 +1,7 @@ +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = async ({ locals }) => { + return { + user: locals.user ?? null + }; +}; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte new file mode 100644 index 0000000..a4de69b --- /dev/null +++ b/src/routes/(app)/+layout.svelte @@ -0,0 +1,16 @@ + + +
+ +
+
+
+ {@render children()} +
+
+
diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte new file mode 100644 index 0000000..486ef1c --- /dev/null +++ b/src/routes/(app)/+page.svelte @@ -0,0 +1,46 @@ + + +
+

+ {#if data.user} + Welcome back, {data.user.name} + {:else} + Welcome to VASH + {/if} +

+

Automated esports match management for osu!

+ + +
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/+layout.svelte b/src/routes/+layout.svelte deleted file mode 100644 index 0d8eb03..0000000 --- a/src/routes/+layout.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - - -{@render children()} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte deleted file mode 100644 index cc88df0..0000000 --- a/src/routes/+page.svelte +++ /dev/null @@ -1,2 +0,0 @@ -

Welcome to SvelteKit

-

Visit svelte.dev/docs/kit to read the documentation

diff --git a/src/routes/layout.css b/src/routes/layout.css deleted file mode 100644 index 1c4d2a8..0000000 --- a/src/routes/layout.css +++ /dev/null @@ -1,2 +0,0 @@ -@import 'tailwindcss'; -@plugin '@tailwindcss/typography'; diff --git a/src/routes/page.svelte.spec.ts b/src/routes/page.svelte.spec.ts deleted file mode 100644 index d54f00a..0000000 --- a/src/routes/page.svelte.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { page } from 'vitest/browser'; -import { describe, expect, it } from 'vitest'; -import { render } from 'vitest-browser-svelte'; -import Page from './+page.svelte'; - -describe('/+page.svelte', () => { - it('should render h1', async () => { - render(Page); - - const heading = page.getByRole('heading', { level: 1 }); - await expect.element(heading).toBeInTheDocument(); - }); -}); From 0f08bd5d22ce60532ffbea7b02b31dec74c99d4f Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Sat, 28 Feb 2026 01:57:48 +0100 Subject: [PATCH 09/14] vash esports branding --- src/app.html | 1 + static/android-chrome-192x192.png | Bin 0 -> 38272 bytes static/android-chrome-512x512.png | Bin 0 -> 159273 bytes static/apple-touch-icon.png | Bin 0 -> 36949 bytes static/favicon-16x16.png | Bin 0 -> 860 bytes static/favicon-32x32.png | Bin 0 -> 2419 bytes static/favicon.ico | Bin 0 -> 15406 bytes static/site.webmanifest | 1 + 8 files changed, 2 insertions(+) create mode 100644 static/android-chrome-192x192.png create mode 100644 static/android-chrome-512x512.png create mode 100644 static/apple-touch-icon.png create mode 100644 static/favicon-16x16.png create mode 100644 static/favicon-32x32.png create mode 100644 static/favicon.ico create mode 100644 static/site.webmanifest diff --git a/src/app.html b/src/app.html index e0a3d41..e57255a 100644 --- a/src/app.html +++ b/src/app.html @@ -9,6 +9,7 @@ href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700;800&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet" /> + Vash Esports %sveltekit.head% diff --git a/static/android-chrome-192x192.png b/static/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..e15a9abf8b8deee63ec91128231a0d65918364c4 GIT binary patch literal 38272 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 z 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 UX#KGOX8-^I07*qoM6N<$g30ad4gdfE literal 0 HcmV?d00001 diff --git a/static/android-chrome-512x512.png b/static/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..58bc4cbffcf157d3b847fa7971259fad0f40a210 GIT binary patch literal 159273 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 z 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 !D?Ru;}|)M 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 literal 0 HcmV?d00001 diff --git a/static/apple-touch-icon.png b/static/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fb8b64b537048492de285f22519de261c0892b1f GIT binary patch literal 36949 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 QqcQ&f`~RPTf7J~9FZj=DC;$Ke literal 0 HcmV?d00001 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 From 133b9e552b67c9b6e4ab85cb1e3df50444556293 Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Sat, 28 Feb 2026 01:58:03 +0100 Subject: [PATCH 10/14] mappool ui --- src/routes/(app)/mappools/+page.server.ts | 32 +++ src/routes/(app)/mappools/+page.svelte | 66 ++++++ .../(app)/mappools/[id]/+page.server.ts | 118 ++++++++++ src/routes/(app)/mappools/[id]/+page.svelte | 205 ++++++++++++++++++ 4 files changed, 421 insertions(+) create mode 100644 src/routes/(app)/mappools/+page.server.ts create mode 100644 src/routes/(app)/mappools/+page.svelte create mode 100644 src/routes/(app)/mappools/[id]/+page.server.ts create mode 100644 src/routes/(app)/mappools/[id]/+page.svelte 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 @@ + + + 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..e2a05b0 --- /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' }; + } + + // Parse URL if needed + 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]; + + // Strip non-numeric + if (!/^\d+$/.test(beatmapId)) { + return { error: 'Invalid beatmap ID' }; + } + + try { + 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, + 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} +
+
From e147ba4432f4a9351d27388ec4cb7c926427b245 Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Sat, 28 Feb 2026 04:35:21 +0100 Subject: [PATCH 11/14] osu match playing --- bun.lock | 9 + package.json | 1 + src/lib/components/Sidebar.svelte | 2 +- src/lib/server/bancho/client.ts | 281 ++++++++++ src/lib/server/db/schema.ts | 36 +- src/lib/server/match/engine.ts | 523 ++++++++++++++++++ src/lib/server/match/orchestrator.ts | 331 +++++++++++ src/lib/server/match/types.ts | 24 + src/routes/(app)/+layout.server.ts | 2 +- src/routes/(app)/+page.server.ts | 52 ++ src/routes/(app)/+page.svelte | 224 ++++++-- .../(app)/mappools/[id]/+page.server.ts | 14 +- src/routes/(app)/matches/+page.server.ts | 71 +++ src/routes/(app)/matches/+page.svelte | 188 +++++++ src/routes/(app)/matches/[id]/+page.server.ts | 136 +++++ src/routes/(app)/matches/[id]/+page.svelte | 389 +++++++++++++ src/routes/api/matches/+server.ts | 41 ++ src/routes/api/matches/[id]/+server.ts | 25 + src/routes/api/matches/[id]/lobby/+server.ts | 16 + src/routes/api/matches/[id]/pick/+server.ts | 19 + src/routes/api/matches/[id]/roll/+server.ts | 19 + .../api/matches/[id]/rolling/+server.ts | 14 + src/routes/api/matches/[id]/score/+server.ts | 19 + src/routes/api/queue/+server.ts | 42 ++ 24 files changed, 2433 insertions(+), 45 deletions(-) create mode 100644 src/lib/server/bancho/client.ts create mode 100644 src/lib/server/match/engine.ts create mode 100644 src/lib/server/match/orchestrator.ts create mode 100644 src/lib/server/match/types.ts create mode 100644 src/routes/(app)/+page.server.ts create mode 100644 src/routes/(app)/matches/+page.server.ts create mode 100644 src/routes/(app)/matches/+page.svelte create mode 100644 src/routes/(app)/matches/[id]/+page.server.ts create mode 100644 src/routes/(app)/matches/[id]/+page.svelte create mode 100644 src/routes/api/matches/+server.ts create mode 100644 src/routes/api/matches/[id]/+server.ts create mode 100644 src/routes/api/matches/[id]/lobby/+server.ts create mode 100644 src/routes/api/matches/[id]/pick/+server.ts create mode 100644 src/routes/api/matches/[id]/roll/+server.ts create mode 100644 src/routes/api/matches/[id]/rolling/+server.ts create mode 100644 src/routes/api/matches/[id]/score/+server.ts create mode 100644 src/routes/api/queue/+server.ts diff --git a/bun.lock b/bun.lock index 4756f0c..17c43ca 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "esports", "dependencies": { + "bancho.js": "^0.12.3", "postgres": "^3.4.8", }, "devDependencies": { @@ -330,6 +331,8 @@ "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=="], @@ -514,6 +517,8 @@ "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=="], @@ -536,6 +541,8 @@ "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=="], @@ -588,6 +595,8 @@ "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=="], diff --git a/package.json b/package.json index abefed1..e08ff9e 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "vitest-browser-svelte": "^2.0.2" }, "dependencies": { + "bancho.js": "^0.12.3", "postgres": "^3.4.8" } } diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index 2712955..f7100d7 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -24,7 +24,7 @@ > V - VASH + Vash Esports 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/schema.ts b/src/lib/server/db/schema.ts index 6caa026..b3d5276 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -10,6 +10,7 @@ import { uniqueIndex, real } from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; export * from './auth.schema'; @@ -51,6 +52,7 @@ export const mappoolSlot = pgTable('mappool_slot', { 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() }); @@ -91,7 +93,7 @@ export const matchParticipant = pgTable( pickOrder: integer('pick_order') }, (t) => [ - uniqueIndex('idx_mp_match_team').on(t.matchId, t.teamId), + 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) ] @@ -158,7 +160,33 @@ export const matchGameScore = pgTable( (t) => [index('idx_mgs_game').on(t.matchGameId)] ); -import { relations } from 'drizzle-orm'; +// ── 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 ─────────────────────────────────────────────────────────── @@ -210,3 +238,7 @@ export const matchGameScoreRelations = relations(matchGameScore, ({ one }) => ({ 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/routes/(app)/+layout.server.ts b/src/routes/(app)/+layout.server.ts index c9d39ee..a971253 100644 --- a/src/routes/(app)/+layout.server.ts +++ b/src/routes/(app)/+layout.server.ts @@ -1,4 +1,4 @@ -import type { LayoutServerLoad } from './$types'; +import type { LayoutServerLoad } from "./$types"; export const load: LayoutServerLoad = async ({ locals }) => { return { diff --git a/src/routes/(app)/+page.server.ts b/src/routes/(app)/+page.server.ts new file mode 100644 index 0000000..93020e9 --- /dev/null +++ b/src/routes/(app)/+page.server.ts @@ -0,0 +1,52 @@ +import { db } from '$lib/server/db'; +import { match, playerRating, matchQueue } from '$lib/server/db/schema'; +import { eq, desc, sql } from 'drizzle-orm'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ locals }) => { + if (!locals.user) { + return { user: null, rating: null, queueStatus: null, recentMatches: [], poolCount: 0 }; + } + + const rating = await db.query.playerRating.findFirst({ + where: eq(playerRating.userId, locals.user.id) + }); + + const queueEntry = await db.query.matchQueue.findFirst({ + where: eq(matchQueue.userId, locals.user.id) + }); + + const queueCount = await db + .select({ count: sql`count(*)` }) + .from(matchQueue); + + const poolCount = await db + .select({ count: sql`count(*)` }) + .from(db._.fullSchema.mappool); + + const recentMatches = await db.query.match.findMany({ + with: { + participants: { + with: { team: true, players: true } + } + }, + orderBy: desc(match.createdAt), + limit: 10 + }); + + const userMatches = recentMatches.filter((m) => + m.participants.some((p) => p.players.some((pl) => pl.userId === locals.user!.id)) + ); + + return { + user: locals.user, + rating: rating ?? { elo: 1000, wins: 0, losses: 0 }, + queueStatus: { + inQueue: !!queueEntry, + queueSize: Number(queueCount[0].count), + joinedAt: queueEntry?.joinedAt ?? null + }, + recentMatches: userMatches, + poolCount: Number(poolCount[0].count) + }; +}; diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index 486ef1c..fd0738d 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/+page.svelte @@ -3,44 +3,200 @@ import './layout.css'; let { data }: PageProps = $props(); + + let joining = $state(false); + let leaving = $state(false); + let queueTimer = $state(''); + let queueInterval: ReturnType | null = null; + + $effect(() => { + if (data.queueStatus?.inQueue && data.queueStatus.joinedAt) { + const start = new Date(data.queueStatus.joinedAt).getTime(); + queueInterval = setInterval(() => { + const elapsed = Math.floor((Date.now() - start) / 1000); + const m = Math.floor(elapsed / 60); + const s = elapsed % 60; + queueTimer = `${m}:${s.toString().padStart(2, '0')}`; + }, 1000); + } + + return () => { + if (queueInterval) clearInterval(queueInterval); + }; + }); + + async function joinQueue() { + joining = true; + try { + const res = await fetch('/api/queue', { method: 'POST' }); + const result = await res.json(); + if (result.matched && result.match) { + window.location.href = `/matches/${result.match.id}`; + } else { + window.location.reload(); + } + } catch { + joining = false; + } + } + + async function leaveQueue() { + leaving = true; + await fetch('/api/queue', { method: 'DELETE' }); + window.location.reload(); + } + + function stateLabel(state: string) { + const map: Record = { + CREATED: 'Created', LOBBY: 'In Lobby', ROLLING: 'Rolling', + PICKING: 'Picking', PLAYING: 'Playing', FINISHED: 'Finished', CANCELLED: 'Cancelled' + }; + return map[state] ?? state; + } + + function stateColor(state: string) { + const map: Record = { + CREATED: 'text-text-secondary', LOBBY: 'text-yellow-400', ROLLING: 'text-yellow-400', + PICKING: 'text-blue-400', PLAYING: 'text-green-400', FINISHED: 'text-text-secondary', CANCELLED: 'text-red-400' + }; + return map[state] ?? 'text-text-secondary'; + }
-

- {#if data.user} - Welcome back, {data.user.name} - {:else} - Welcome to VASH - {/if} -

-

Automated esports match management for osu!

+ {#if !data.user} +
+

VASH

+

Automated esports match management for osu!

+ + Sign in with osu! + +
+ {:else} + +
+

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

+
+
+

ELO

+

{data.rating?.elo ?? 1000}

+
+
+

W / L

+

+ {data.rating?.wins ?? 0} + / + {data.rating?.losses ?? 0} +

+
+
+
-
- -
⚔
-

Matches

-

Create and manage competitive matches

-
+ +
+ {#if data.queueStatus?.inQueue} +
+
+

In Queue

+

+ Searching for opponent... + {queueTimer} +

+

+ {data.queueStatus.queueSize} player{data.queueStatus.queueSize !== 1 ? 's' : ''} in queue +

+
+ +
+
+
+
+
+
+ Matchmaking active +
+ {:else} +
+
+

Ranked Queue

+

+ {#if data.poolCount > 0} + 1v1 · Best of 7 · + {data.queueStatus?.queueSize ?? 0} searching + {:else} + No mappools available — create one first + {/if} +

+
+ +
+ {/if} +
- -
♫
-

Mappools

-

Build and share beatmap pools

-
+ +
+

Recent Matches

+
+ {#each data.recentMatches as m} + {@const p1 = m.participants[0]} + {@const p2 = m.participants[1]} + {@const isUserP1 = p1?.players.some((pl) => pl.userId === data.user?.id)} + {@const userP = isUserP1 ? p1 : p2} + {@const oppP = isUserP1 ? p2 : p1} + {@const won = m.winnerId === userP?.teamId} + {@const lost = m.winnerId && m.winnerId !== userP?.teamId} - -
⚑
-

Teams

-

Manage your teams and rosters

-
-
+ +
+
+
+ {m.name} + {stateLabel(m.state)} +
+
+ {userP?.team.name ?? '?'} vs {oppP?.team.name ?? '?'} +
+
+ {#if p1 && p2} +
+ {p1.score} + - + {p2.score} +
+ {/if} + {new Date(m.createdAt).toLocaleDateString()} +
+ {:else} +
+

No matches yet. Join the queue!

+
+ {/each} +
+
+ + + + {/if}
diff --git a/src/routes/(app)/mappools/[id]/+page.server.ts b/src/routes/(app)/mappools/[id]/+page.server.ts index e2a05b0..5eb2b71 100644 --- a/src/routes/(app)/mappools/[id]/+page.server.ts +++ b/src/routes/(app)/mappools/[id]/+page.server.ts @@ -63,19 +63,18 @@ export const actions: Actions = { return { error: 'Beatmap ID and category are required' }; } - // Parse URL if needed - 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]; + 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]; - // Strip non-numeric if (!/^\d+$/.test(beatmapId)) { return { error: 'Invalid beatmap ID' }; } + let beatmap; try { - await getBeatmap(beatmapId); + beatmap = await getBeatmap(beatmapId); } catch { return { error: 'Beatmap not found on osu!' }; } @@ -92,6 +91,7 @@ export const actions: Actions = { beatmapId, category, orderInCategory: existing.length + 1, + starRating: beatmap.difficulty_rating, mods: category === 'NM' || category === 'TB' || category === 'FM' ? [] : [category] }); 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..482c62f --- /dev/null +++ b/src/routes/(app)/matches/+page.svelte @@ -0,0 +1,188 @@ + + +
+
+
+

Matches

+

All matches

+
+ +
+ + + {#if creating} +
{ + createError = ''; + return async ({ result, update }) => { + if (result.type === 'failure') { + createError = (result.data as { error?: string })?.error ?? 'Failed to create match'; + } else { + await update(); + } + }; + }} + class="mt-4 rounded-lg border border-border bg-surface-800 p-5" + > +

Create Match

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + {#if createError} +

{createError}

+ {/if} + + +
+ {/if} + + +
+ {#each data.matches as m} + {@const p1 = m.participants[0]} + {@const p2 = m.participants[1]} + +
+
+ {m.name} + + {m.state} + +
+

+ {p1?.team.name ?? '?'} vs {p2?.team.name ?? '?'} + · {new Date(m.createdAt).toLocaleDateString()} +

+
+ + {#if p1 && p2} +
+ {p1.score} + - + {p2.score} +
+ {/if} +
+ {:else} +
+

No matches yet

+
+ {/each} +
+
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..f69cdaf --- /dev/null +++ b/src/routes/(app)/matches/[id]/+page.server.ts @@ -0,0 +1,136 @@ +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'; + +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 = { + 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..76aabc0 --- /dev/null +++ b/src/routes/(app)/matches/[id]/+page.svelte @@ -0,0 +1,389 @@ + + +
+ +
+ ← +
+

{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 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/api/matches/+server.ts b/src/routes/api/matches/+server.ts new file mode 100644 index 0000000..abf3e5e --- /dev/null +++ b/src/routes/api/matches/+server.ts @@ -0,0 +1,41 @@ +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) { + 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..b712ae4 --- /dev/null +++ b/src/routes/api/matches/[id]/+server.ts @@ -0,0 +1,25 @@ +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) { + 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) { + 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..bcd4c93 --- /dev/null +++ b/src/routes/api/matches/[id]/lobby/+server.ts @@ -0,0 +1,16 @@ +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) { + 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..808679f --- /dev/null +++ b/src/routes/api/matches/[id]/pick/+server.ts @@ -0,0 +1,19 @@ +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) { + 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..d2bf742 --- /dev/null +++ b/src/routes/api/matches/[id]/roll/+server.ts @@ -0,0 +1,19 @@ +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) { + 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..b3b538e --- /dev/null +++ b/src/routes/api/matches/[id]/rolling/+server.ts @@ -0,0 +1,14 @@ +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) { + 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..8ceafe1 --- /dev/null +++ b/src/routes/api/matches/[id]/score/+server.ts @@ -0,0 +1,19 @@ +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) { + error(400, e.message); + } +}; diff --git a/src/routes/api/queue/+server.ts b/src/routes/api/queue/+server.ts new file mode 100644 index 0000000..6715e2e --- /dev/null +++ b/src/routes/api/queue/+server.ts @@ -0,0 +1,42 @@ +import { json, error } from '@sveltejs/kit'; +import { joinQueue, leaveQueue, getQueueStatus } from '$lib/server/match/engine'; +import { db } from '$lib/server/db'; +import { team, teamMember } from '$lib/server/db/schema'; +import { eq, and } 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) { + 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 }); +}; From f4b94597c7052856fe767a376c1dedb38a599dfa Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Sat, 28 Feb 2026 05:03:11 +0100 Subject: [PATCH 12/14] feat: implement team management features including team creation, member addition, and deletion - Added a new page for managing teams with functionalities to create teams, add members, and delete teams. - Enhanced the matches page with improved match creation form and live match display. - Refactored state handling for match states and improved UI for displaying match information. --- src/routes/(app)/+layout.svelte | 1 + src/routes/(app)/+page.server.ts | 68 ++--- src/routes/(app)/+page.svelte | 336 +++++++++++----------- src/routes/(app)/matches/+page.svelte | 383 +++++++++++++++---------- src/routes/(app)/teams/+page.server.ts | 120 ++++++++ src/routes/(app)/teams/+page.svelte | 229 +++++++++++++++ 6 files changed, 777 insertions(+), 360 deletions(-) create mode 100644 src/routes/(app)/teams/+page.server.ts create mode 100644 src/routes/(app)/teams/+page.svelte diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index a4de69b..51b9599 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -1,4 +1,5 @@ -
- {#if !data.user} -
-

VASH

-

Automated esports match management for osu!

- - Sign in with osu! - -
- {:else} - -
-

+
+ +
+

+ {#if data.user} Welcome back, {data.user.name} -

-
-
-

ELO

-

{data.rating?.elo ?? 1000}

-
-
-

W / L

-

- {data.rating?.wins ?? 0} - / - {data.rating?.losses ?? 0} -

+ {:else} + Welcome to VASH + {/if} +

+

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} + +
+ + - -
- {#if data.queueStatus?.inQueue} -
-
-

In Queue

-

- Searching for opponent... - {queueTimer} -

-

- {data.queueStatus.queueSize} player{data.queueStatus.queueSize !== 1 ? 's' : ''} in queue -

-
- -
-
-
-
-
-
- Matchmaking active -
- {:else} -
-
-

Ranked Queue

-

- {#if data.poolCount > 0} - 1v1 · Best of 7 · - {data.queueStatus?.queueSize ?? 0} searching - {:else} - No mappools available — create one first - {/if} -

-
- -
- {/if} -
- - -
diff --git a/src/routes/(app)/matches/+page.svelte b/src/routes/(app)/matches/+page.svelte index 482c62f..f45faec 100644 --- a/src/routes/(app)/matches/+page.svelte +++ b/src/routes/(app)/matches/+page.svelte @@ -2,187 +2,272 @@ import { enhance } from '$app/forms'; let { data } = $props(); - let creating = $state(false); + + let showCreate = $state(false); let createError = $state(''); - function stateColor(state: string) { - const colors: Record = { - CREATED: 'border-border text-text-secondary', - LOBBY: 'border-yellow-500/30 text-yellow-400', - ROLLING: 'border-yellow-500/30 text-yellow-400', - PICKING: 'border-blue-500/30 text-blue-400', - PLAYING: 'border-green-500/30 text-green-400', - FINISHED: 'border-border text-text-secondary', - CANCELLED: 'border-red-500/30 text-red-400' - }; - return colors[state] ?? 'border-border text-text-secondary'; + const stateConfig: Record = { + CREATED: { label: 'Created', color: 'text-text-secondary border-border' }, + LOBBY: { label: 'In Lobby', color: 'text-yellow-400 border-yellow-500/30' }, + ROLLING: { label: 'Rolling', color: 'text-yellow-400 border-yellow-500/30', dot: 'bg-yellow-400' }, + PICKING: { label: 'Picking', color: 'text-blue-400 border-blue-500/30', dot: 'bg-blue-400' }, + PLAYING: { label: 'Live', color: 'text-green-400 border-green-500/30', dot: 'bg-green-400' }, + FINISHED: { label: 'Finished', color: 'text-text-secondary border-border' }, + CANCELLED: { label: 'Cancelled', color: 'text-red-400 border-red-500/30' } + }; + + const liveStates = ['LOBBY', 'ROLLING', 'PICKING', 'PLAYING']; + + const liveMatches = $derived(data.matches.filter((m: any) => liveStates.includes(m.state))); + const recentMatches = $derived(data.matches.filter((m: any) => !liveStates.includes(m.state))); + + function teamDisplay(m: any) { + const p1 = m.participants[0]; + const p2 = m.participants[1]; + return { p1, p2 }; } - function poolLabel(pool: { name: string; slots: { starRating: number | null }[] }) { - if (pool.slots.length === 0) return pool.name; - const avg = pool.slots.reduce((s, sl) => s + (sl.starRating ?? 0), 0) / pool.slots.length; - return `${pool.name} (★${avg.toFixed(1)}, ${pool.slots.length} maps)`; + function timeAgo(date: string | Date) { + const d = new Date(date); + const diff = Date.now() - d.getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return 'just now'; + if (mins < 60) return `${mins}m ago`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; } -
+
+

Matches

-

All matches

+

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

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

Create Match

-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- {#if createError} -

{createError}

+

{createError}

{/if} - +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +
{/if} - -
- {#each data.matches as m} - {@const p1 = m.participants[0]} - {@const p2 = m.participants[1]} - -
-
- {m.name} - - {m.state} - -
-

- {p1?.team.name ?? '?'} vs {p2?.team.name ?? '?'} - · {new Date(m.createdAt).toLocaleDateString()} -

+ +{#if liveMatches.length > 0} +
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} +
From 90396b5338794824ad32d90f30e5aa5b654602d9 Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Sat, 28 Feb 2026 05:49:56 +0100 Subject: [PATCH 13/14] settings + reinvite players to lobby --- src/app.d.ts | 11 +- src/hooks.server.ts | 15 +- src/lib/components/Header.svelte | 23 +- src/lib/components/Sidebar.svelte | 28 +- src/lib/server/auth.ts | 6 +- src/lib/server/db/auth.schema.ts | 149 ++++---- src/lib/server/db/schema.ts | 5 + src/lib/server/osu/api.ts | 31 +- src/lib/server/osu/cache.ts | 42 +++ src/lib/server/permissions.ts | 56 +++ src/routes/(app)/+layout.server.ts | 13 +- src/routes/(app)/+layout.svelte | 43 ++- src/routes/(app)/+page.server.ts | 19 +- src/routes/(app)/+page.svelte | 348 ++++++++++-------- src/routes/(app)/admin/+page.server.ts | 61 +++ src/routes/(app)/admin/+page.svelte | 103 ++++++ src/routes/(app)/matches/+page.svelte | 4 +- src/routes/(app)/matches/[id]/+page.server.ts | 26 ++ src/routes/(app)/matches/[id]/+page.svelte | 21 ++ src/routes/(app)/settings/+page.server.ts | 46 +++ src/routes/(app)/settings/+page.svelte | 114 ++++++ src/routes/+error.svelte | 29 ++ src/routes/api/health/+server.ts | 27 ++ src/routes/api/matches/+server.ts | 3 +- src/routes/api/matches/[id]/+server.ts | 5 +- src/routes/api/matches/[id]/lobby/+server.ts | 3 +- src/routes/api/matches/[id]/pick/+server.ts | 3 +- src/routes/api/matches/[id]/roll/+server.ts | 3 +- .../api/matches/[id]/rolling/+server.ts | 3 +- src/routes/api/matches/[id]/score/+server.ts | 3 +- src/routes/api/queue/+server.ts | 7 +- src/routes/login/+page.server.ts | 18 + 32 files changed, 1000 insertions(+), 268 deletions(-) create mode 100644 src/lib/server/osu/cache.ts create mode 100644 src/lib/server/permissions.ts create mode 100644 src/routes/(app)/admin/+page.server.ts create mode 100644 src/routes/(app)/admin/+page.svelte create mode 100644 src/routes/(app)/settings/+page.server.ts create mode 100644 src/routes/(app)/settings/+page.svelte create mode 100644 src/routes/+error.svelte create mode 100644 src/routes/api/health/+server.ts create mode 100644 src/routes/login/+page.server.ts diff --git a/src/app.d.ts b/src/app.d.ts index 2ae6f13..d0a7b01 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,15 +1,18 @@ +// src/app.d.ts import type { User, Session } from 'better-auth/minimal'; -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces declare global { namespace App { interface Locals { - user?: User; + user?: User & { role: string }; session?: Session; } - // interface Error {} + interface Error { + message: string; + code?: string; + } + // interface PageData {} // interface PageState {} // interface Platform {} diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 22e93f9..bc8ea8a 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,14 +1,27 @@ +// 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; - event.locals.user = session.user; + + // 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 }); diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index ed6a9f3..a0e5148 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -1,7 +1,15 @@
{#if user}
+ {#if badge} + + {badge.label} + + {/if} {#if user.image} {user.name} {/if} {user.name} +
+ +
{:else} Sign in with osu! diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index f7100d7..a1493b9 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -1,6 +1,8 @@