diff --git a/.cspell.json b/.cspell.json index 98a900be..57ebb38e 100644 --- a/.cspell.json +++ b/.cspell.json @@ -4,18 +4,28 @@ "words": [ "autoincrement", "bunx", + "chrono", "coursemate", + "daisyui", "datasource", "direnv", "ECCS", "isready", "lockb", + "mytheme", "notistack", "psql", "qiita", + "reqwest", "safify", "supabase", - "swiper" + "swiper", + "lefthook", + "stdenv", + "rustc", + "pkgs", + "nixpkgs", + "libquery" ], "dictionaries": [ "softwareTerms", @@ -31,6 +41,7 @@ ], "ignorePaths": [ "flake.*", + "nix/*", "**/.git/**", "**/node_modules/**", "server/target/**", @@ -39,6 +50,11 @@ "**/bun.lockb", "**/*.svg", "**/migration.sql", - "**/data.json" + "**/data.json", + "**/Cargo.*", + "scraper/target", + "**/rust-toolchain.toml", + "web/out", + "**/tsconfig.tsbuildinfo" ] } diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..6a0d87ef --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +.env* diff --git a/.env.sample b/.env.sample new file mode 100644 index 00000000..1e593430 --- /dev/null +++ b/.env.sample @@ -0,0 +1 @@ +SQL_GENERATE_URL=postgres://sql-url diff --git a/.envrc b/.envrc index d88fbcde..6b3f1a6b 100644 --- a/.envrc +++ b/.envrc @@ -1,3 +1,4 @@ +watch_file ./server/prisma.nix # I recommend using nix-direnv: https://github.com/nix-community/nix-direnv if command -v nix; then diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 28926e86..90a1a3ff 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,21 +1,12 @@ - # PRの概要 - - - - ## 具体的な変更内容 - ## 影響範囲 - ## 動作要件 - ## 補足 - ## レビューリクエストを出す前にチェック! diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 102c2e26..0eaf48df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,17 +7,39 @@ on: env: DATABASE_URL: ${{ secrets.DATABASE_URL_FOR_PRISMA_SQL_GENERATION }} + DIRECT_URL: ${{ secrets.DATABASE_URL_FOR_PRISMA_SQL_GENERATION }} + NEXT_PUBLIC_API_ENDPOINT: "sample" + NEXT_PUBLIC_FIREBASE_API_KEY: "sample" + NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: "sample" + NEXT_PUBLIC_FIREBASE_PROJECT_ID: "sample" + NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: "sample" + NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: "sample" + NEXT_PUBLIC_FIREBASE_APP_ID: "sample" + NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: "sample" jobs: + sync-db: + runs-on: ubuntu-latest + name: Build + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - run: bun i --frozen-lockfile + - run: bunx prisma db push + working-directory: server + build: name: Build runs-on: ubuntu-latest + needs: [sync-db] steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 with: bun-version: latest - - run: make setup-ci - - run: make build + - run: bun install --frozen-lockfile + - run: bun run build biome: name: Code style check via Biome @@ -31,8 +53,8 @@ jobs: with: version: latest - - run: make setup - - run: biome check + - run: bun install --frozen-lockfile + - run: bun style:check type-check: name: Type Check @@ -42,12 +64,8 @@ jobs: - uses: oven-sh/setup-bun@v2 with: bun-version: latest - - run: make setup-ci - - - run: bunx tsc --noEmit - working-directory: web - - run: bunx tsc --noEmit - working-directory: server + - run: bun install --frozen-lockfile + - run: bun type spell-check: name: Spell Check @@ -58,33 +76,24 @@ jobs: with: bun-version: latest - - run: make setup-ci - - run: make spell-check + - run: bun install --frozen-lockfile + - run: bun spell . test: name: Bun Test runs-on: ubuntu-latest + needs: [sync-db] steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - - run: make setup-ci - - run: make test + - run: bun install --frozen-lockfile + - run: bun run test deploy-test-web: name: Deploy Test (web) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 - - uses: oven-sh/setup-bun@v2 - - run: make prepare-deploy-web - - run: test `ls web/dist | wc -l` != 0 - - deploy-test-server: - name: Deploy Test (server) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 - uses: oven-sh/setup-bun@v2 - - run: make prepare-deploy-server + - run: bun prepare:deploy:web + - run: test `ls web/.next | wc -l` != 0 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..39a32e82 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,17 @@ +name: Deploy +on: + push: + branches: + - release + +jobs: + deploy: + name: Deploy Server to Fly.io + runs-on: ubuntu-latest + env: + SQL_GENERATE_URL: ${{ secrets.DATABASE_URL_FOR_PRISMA_SQL_GENERATION }} + + steps: + - uses: actions/checkout@v4 + - uses: superfly/flyctl-actions/setup-flyctl@master + - run: flyctl deploy --build-arg SQL_GENERATE_URL=$SQL_GENERATE_URL --access-token "${{ secrets.FLY_DEPLOY_TOKEN }}" diff --git a/.github/workflows/keep-alive.yml b/.github/workflows/keep-alive.yml index 31cd7b58..2c172179 100644 --- a/.github/workflows/keep-alive.yml +++ b/.github/workflows/keep-alive.yml @@ -2,7 +2,7 @@ name: Keep Alive on: schedule: - - cron: "*/5 * * * *" + - cron: "*/2 * * * *" workflow_dispatch: env: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 00000000..da0190af --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,37 @@ +name: Rust +on: + pull_request: + paths: + - scraper/** + push: + branches: [main] + paths: + - scraper/** + +jobs: + all: + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + RUSTFLAGS: -D warnings + defaults: + run: + working-directory: scraper + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/ + ~/.rustup/ + scraper/target + key: cargo-cache-${{ github.job }}-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + - uses: dtolnay/rust-toolchain@v1 + with: + toolchain: 1.82.0 + components: clippy,rustfmt + - run: cargo build + - run: cargo build --release + - run: cargo test + - run: cargo clippy + - run: cargo fmt --check diff --git a/.gitignore b/.gitignore index 7894de25..0535f467 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +/node_modules +.env +/.direnv +/.husky + # Logs logs *.log @@ -7,11 +12,6 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* -node_modules -dist -dist-ssr -*.local - # Editor directories and files .vscode/* !.vscode/extensions.json @@ -23,5 +23,4 @@ dist-ssr *.sln *.sw? -.env -/.direnv +tsconfig.tsbuildinfo diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index c82ad188..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1 +0,0 @@ -make precommit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..80b12fec --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# syntax = docker/dockerfile:1 + +ARG BUN_VERSION=1.2.2 +FROM oven/bun:${BUN_VERSION} AS base +LABEL fly_launch_runtime="Bun/Prisma" +ENV NODE_ENV="production" + +# Throw-away build stage to reduce size of final image +FROM base AS build + +WORKDIR /build +ARG SQL_GENERATE_URL +RUN test -n "${SQL_GENERATE_URL}" +ENV DATABASE_URL=$SQL_GENERATE_URL +ENV DIRECT_URL=$SQL_GENERATE_URL +COPY . . +RUN --mount=type=cache,target=~/.bun/install bun install --frozen-lockfile --ignore-scripts +RUN cd server; bun prisma generate --sql +RUN cd server; bun run :build + + +# Final stage for app image +FROM base AS runner +WORKDIR /srv + +# Copy built application +COPY --from=build /build/server/target/index.js /srv/index.js +COPY --from=build /build/node_modules/.prisma/client /node_modules/.prisma/client +COPY --from=build /build/node_modules/@img /node_modules/@img + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD [ "bun", "run", "./index.js" ] diff --git a/Makefile b/Makefile index 419a455e..8d379037 100644 --- a/Makefile +++ b/Makefile @@ -2,66 +2,19 @@ default: start LOCAL_DB := postgres://user:password@localhost:5432/database -setup: - if [ ! `command -v bun` ]; then echo 'ERR: Bun is required!'; exit 1; fi - make sync - bunx husky - cd web; if [ ! -f .env ]; then cp ./.env.sample ./.env ; fi - cd server; if [ ! -f .env.dev ]; then cp ./.env.sample ./.env.dev ; fi - @echo "auto setup is done. now do:" - @echo "- edit server/.env.dev" - @echo "- edit web/.env" - @echo "- run make sync" - -setup-ci: - if [ ${DATABASE_URL} == "" ]; then echo 'Please set DATABASE_URL_FOR_SQL_GENERATION!'; exit 1; fi - make sync - make generate-sql - -sync: sync-server sync-web sync-root copy-common - @echo '----------------------------------------------------------------------------------------------------------' - @echo '| Most work is done. now running prisma-generate-sql (which might fail if .env.dev is not set configured)|' - @echo '----------------------------------------------------------------------------------------------------------' - make generate-sql || true - -generate-sql: - @cd server; bun run prisma-generate-sql - -start: start-all # build -> serve -build: build-server build-web -serve: serve-all # serve only. does not build. -watch: - (trap 'kill 0' SIGINT; make watch-web & make watch-server & wait) - - test: export DATABASE_URL=$(LOCAL_DB) -test: export NEVER_LOAD_DOTENV=1 +test: export DIRECT_URL=$(LOCAL_DB) test: export UNSAFE_SKIP_AUTH=1 test: export FIREBASE_PROJECT_ID=mock-proj +test: export CORS_ALLOW_ORIGINS=http://localhost:3000,https://localhost:5173 test: dev-db - cd server/src; ENV_FILE=../.env.dev bun test - cd ./test; ENV_FILE=../server/.env.dev bun test + cd server/src; ENV_FILE=none bun test + cd ./test; ENV_FILE=none bun test docker stop postgres -prepare-deploy-web: copy-common - cd web; bun install; bun run build -prepare-deploy-server: copy-common - cd server; bun install; npx prisma generate; -deploy-server: - cd server; bun src/index.ts - -docker: copy-common - @# deferring `docker compose down`. https://qiita.com/KEINOS/items/532dc395fe0f89c2b574 - trap 'docker compose down' EXIT; docker compose up --build - -docker-watch: copy-common - docker compose up --build --watch - -seed: - cd server; bunx prisma db seed - ## server/.envをDATABASE_URL=postgres://user:password@localhost:5432/databaseにしてから行う dev-db: export DATABASE_URL=$(LOCAL_DB) +dev-db: export DIRECT_URL=$(LOCAL_DB) dev-db: export NEVER_LOAD_DOTENV=1 dev-db: docker stop postgres || true @@ -72,102 +25,13 @@ dev-db: -e POSTGRES_DB=database \ postgres:alpine @echo "Waiting for PostgreSQL to be ready..." - @sleep 5 # PostgreSQLが起動するまでの待機(必要に応じて調整) + @sleep 2 # PostgreSQLが起動するまでの待機(必要に応じて調整) @until docker exec postgres pg_isready -U user -d database; do \ echo "Waiting for PostgreSQL to be ready..."; \ sleep 1; \ done @echo "PostgreSQL is ready. Running seed..." - @cd server; bunx prisma generate; bunx prisma db push; cd .. - @make seed; + @cd server; bunx prisma generate --sql; bunx prisma db push + @bun run seed @echo "Seeding completed." - -precommit: check-branch lint-staged spell-check - -lint-staged: - bunx lint-staged -check-branch: - @ if [ "$(git branch --show-current)" == "main" ]; then echo "Cannot make commit on main! aborting..."; exit 1; fi -spell-check: - bunx cspell --quiet . - -# Sync (install/update packages, generate prisma, etc) - -sync-web: - cd web; bun install - # copy .env.sample -> .env only if .env is not there - -sync-server: - cd server; bun install - cd server; bunx prisma generate - # copy .env.sample -> .env only if .env is not there - -sync-root: - bun install - - -# Static checks - -## code style -style: - if command -v biome; then biome check --write; else bunx @biomejs/biome check --write; fi -style-check: - if command -v biome; then biome check; else bunx @biomejs/biome check; fi - -## Deprecated commands, there warnings will be deleted in the future -lint: - @echo 'DEPRECATED: `make lint` is deprecated. run `make style` instead.' - @exit 1 - -format: - @echo 'DEPRECATED: `make format` is deprecated. run `make style` instead.' - @exit 1 - -format-check: - @echo 'DEPRECATED: `make format-check` is deprecated. run `make style-check` instead.' - @exit 1 - -# type checks -type-check: copy-common - make type-check-server - make type-check-web - -type-check-server: - cd server; bunx tsc --noEmit - -type-check-web: - cd web; bunx tsc --noEmit - - -# Runner - -start-all: build-web build-server - make serve-all - -build-web: copy-common-to-web - cd web; bun run build -build-server: copy-common-to-server - cd server; bun run build - -serve-all: - (trap 'kill 0' EXIT; make serve-web & make serve-server & wait) -serve-web: - cd web; bun run preview # todo: make serve function -serve-server: - cd server; bun run serve - -watch-web: copy-common-to-web - cd web; bun run dev -watch-server: copy-common-to-server - cd server; bun run dev - -copy-common: copy-common-to-server copy-common-to-web -copy-common-to-server: - @ if [ -d server/src/common ]; then rm -r server/src/common; fi - @ cp -r common server/src/common -copy-common-to-web: - @ if [ -d web/src/common ]; then rm -r web/src/common; fi - @ cp -r common web/src/common - -.PHONY: test diff --git a/README.md b/README.md index cfcff829..d3b8945e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ GNU Make が導入されています。以下は、ユーザーが使うこと - make setup (セットアップします。) - make start (build -> serve します。) - make watch (ホットリロードします。) -- make precommit (type-check, format-check, lint を実行します。husky で自動実行されます。) ### 環境構築 @@ -24,9 +23,11 @@ GNU Make が導入されています。以下は、ユーザーが使うこと - 以下をインストールします: - - Bun (js) + - Bun + - Node.js - GNU Make - nvm (optional) + - lefthook - `make setup` を実行します。 @@ -59,3 +60,18 @@ make docker # または make docker-watch ``` + +## Deploy + +web: +```sh +NEXT_PUBLIC_ALLOW_ANY_MAIL_ADDR=true # optional +make prepare-deploy-web` +# serve ./web/dist +``` + +server: +```sh +# prisma がビルド時に DATABASE_URL を要求するため、 root に .env を作って、 .env.sample に従って埋めよう。 +bun deploy:server +``` diff --git a/biome.json b/biome.json index f16f6128..3ff85b17 100644 --- a/biome.json +++ b/biome.json @@ -2,7 +2,15 @@ "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", "formatter": { "indentStyle": "space", - "indentWidth": 2 + "indentWidth": 2, + "lineWidth": 80 + }, + "linter": { + "rules": { + "nursery": { + "useSortedClasses": "warn" + } + } }, "vcs": { "enabled": true, @@ -10,6 +18,15 @@ "useIgnoreFile": true }, "files": { - "ignore": ["bun.lockb", "server/target", "data.json"] + "ignore": [ + "bun.lockb", + "server/target", + "data.json", + "scraper/target", + ".next", + "next-env.d.ts", + "out", + "package-lock.json" + ] } } diff --git a/bun.lock b/bun.lock new file mode 100644 index 00000000..1cc4a83f --- /dev/null +++ b/bun.lock @@ -0,0 +1,1347 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "course-mate", + "devDependencies": { + "@biomejs/biome": "^1.9.1", + "lefthook": "^1.10.10", + "lint-staged": "^15.2.10", + }, + }, + "common": { + "name": "common", + "version": "1.0.0", + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5.0.0", + "zod": "^3.23.8", + }, + }, + "server": { + "name": "server", + "version": "1.0.0", + "dependencies": { + "@hono/zod-validator": "^0.4.3", + "@prisma/client": "5.22.0", + "common": "workspace:common", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "dotenv-cli": "^7.4.2", + "firebase-admin": "^12.2.0", + "hono": "^4.7.1", + "lru-cache": "^11.0.2", + "sharp": "^0.33.5", + "sql-formatter": "^15.4.10", + "zod": "^3.23.8", + }, + "devDependencies": { + "@types/cookie-parser": "^1.4.7", + "@types/cors": "^2.8.17", + "globals": "^15.8.0", + "prisma": "5.22.0", + "typescript": "^5.4.5", + }, + }, + "web": { + "name": "web", + "version": "0.0.0", + "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "@fontsource/roboto": "^5.0.13", + "@hookform/resolvers": "^3.9.1", + "@mui/icons-material": "^5.16.7", + "@mui/material": "^5.15.20", + "common": "workspace:*", + "devalue": "^5.1.1", + "firebase": "^10.12.2", + "framer-motion": "^11.3.23", + "next": "^14.2.16", + "notistack": "^3.0.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-easy-crop": "^5.0.8", + "react-hook-form": "^7.53.2", + "react-icons": "^5.3.0", + "zod": "^3.23.8", + }, + "devDependencies": { + "@types/css-modules": "^1.0.5", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "autoprefixer": "^10.4.20", + "daisyui": "^4.12.14", + "eslint": "^9.14.0", + "globals": "^15.9.0", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.14", + "typescript": "^5.2.2", + }, + }, + }, + "trustedDependencies": [ + "@biomejs/biome", + "lefthook", + ], + "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], + + "@babel/generator": ["@babel/generator@7.26.9", "", { "dependencies": { "@babel/parser": "^7.26.9", "@babel/types": "^7.26.9", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], + + "@babel/parser": ["@babel/parser@7.26.9", "", { "dependencies": { "@babel/types": "^7.26.9" }, "bin": "./bin/babel-parser.js" }, "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A=="], + + "@babel/runtime": ["@babel/runtime@7.26.9", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg=="], + + "@babel/template": ["@babel/template@7.26.9", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.26.9", "@babel/types": "^7.26.9" } }, "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA=="], + + "@babel/traverse": ["@babel/traverse@7.26.9", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.9", "@babel/parser": "^7.26.9", "@babel/template": "^7.26.9", "@babel/types": "^7.26.9", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg=="], + + "@babel/types": ["@babel/types@7.26.9", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw=="], + + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.3.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw=="], + + "@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="], + + "@emotion/cache": ["@emotion/cache@11.14.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA=="], + + "@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], + + "@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.3.1", "", { "dependencies": { "@emotion/memoize": "^0.9.0" } }, "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw=="], + + "@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="], + + "@emotion/react": ["@emotion/react@11.14.0", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA=="], + + "@emotion/serialize": ["@emotion/serialize@1.3.3", "", { "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA=="], + + "@emotion/sheet": ["@emotion/sheet@1.4.0", "", {}, "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="], + + "@emotion/styled": ["@emotion/styled@11.14.0", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/is-prop-valid": "^1.3.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2" }, "peerDependencies": { "@emotion/react": "^11.0.0-rc.0", "react": ">=16.8.0" } }, "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA=="], + + "@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="], + + "@emotion/use-insertion-effect-with-fallbacks": ["@emotion/use-insertion-effect-with-fallbacks@1.2.0", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg=="], + + "@emotion/utils": ["@emotion/utils@1.4.2", "", {}, "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="], + + "@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], + + "@eslint/config-array": ["@eslint/config-array@0.19.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w=="], + + "@eslint/core": ["@eslint/core@0.12.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.0", "", { "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.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ=="], + + "@eslint/js": ["@eslint/js@9.21.0", "", {}, "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.7", "", { "dependencies": { "@eslint/core": "^0.12.0", "levn": "^0.4.1" } }, "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g=="], + + "@fastify/busboy": ["@fastify/busboy@3.1.1", "", {}, "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw=="], + + "@firebase/analytics": ["@firebase/analytics@0.10.8", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/installations": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-CVnHcS4iRJPqtIDc411+UmFldk0ShSK3OB+D0bKD8Ck5Vro6dbK5+APZpkuWpbfdL359DIQUnAaMLE+zs/PVyA=="], + + "@firebase/analytics-compat": ["@firebase/analytics-compat@0.2.14", "", { "dependencies": { "@firebase/analytics": "0.10.8", "@firebase/analytics-types": "0.8.2", "@firebase/component": "0.6.9", "@firebase/util": "1.10.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-unRVY6SvRqfNFIAA/kwl4vK+lvQAL2HVcgu9zTrUtTyYDmtIt/lOuHJynBMYEgLnKm39YKBDhtqdapP2e++ASw=="], + + "@firebase/analytics-types": ["@firebase/analytics-types@0.8.2", "", {}, "sha512-EnzNNLh+9/sJsimsA/FGqzakmrAUKLeJvjRHlg8df1f97NLUlFidk9600y0ZgWOp3CAxn6Hjtk+08tixlUOWyw=="], + + "@firebase/app": ["@firebase/app@0.10.13", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "idb": "7.1.1", "tslib": "^2.1.0" } }, "sha512-OZiDAEK/lDB6xy/XzYAyJJkaDqmQ+BCtOEPLqFvxWKUz5JbBmej7IiiRHdtiIOD/twW7O5AxVsfaaGA/V1bNsA=="], + + "@firebase/app-check": ["@firebase/app-check@0.8.8", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-O49RGF1xj7k6BuhxGpHmqOW5hqBIAEbt2q6POW0lIywx7emYtzPDeQI+ryQpC4zbKX646SoVZ711TN1DBLNSOQ=="], + + "@firebase/app-check-compat": ["@firebase/app-check-compat@0.3.15", "", { "dependencies": { "@firebase/app-check": "0.8.8", "@firebase/app-check-types": "0.5.2", "@firebase/component": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-zFIvIFFNqDXpOT2huorz9cwf56VT3oJYRFjSFYdSbGYEJYEaXjLJbfC79lx/zjx4Fh+yuN8pry3TtvwaevrGbg=="], + + "@firebase/app-check-interop-types": ["@firebase/app-check-interop-types@0.3.2", "", {}, "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ=="], + + "@firebase/app-check-types": ["@firebase/app-check-types@0.5.2", "", {}, "sha512-FSOEzTzL5bLUbD2co3Zut46iyPWML6xc4x+78TeaXMSuJap5QObfb+rVvZJtla3asN4RwU7elaQaduP+HFizDA=="], + + "@firebase/app-compat": ["@firebase/app-compat@0.2.43", "", { "dependencies": { "@firebase/app": "0.10.13", "@firebase/component": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" } }, "sha512-HM96ZyIblXjAC7TzE8wIk2QhHlSvksYkQ4Ukh1GmEenzkucSNUmUX4QvoKrqeWsLEQ8hdcojABeCV8ybVyZmeg=="], + + "@firebase/app-types": ["@firebase/app-types@0.9.2", "", {}, "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ=="], + + "@firebase/auth": ["@firebase/auth@1.7.9", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0", "undici": "6.19.7" }, "peerDependencies": { "@firebase/app": "0.x", "@react-native-async-storage/async-storage": "^1.18.1" }, "optionalPeers": ["@react-native-async-storage/async-storage"] }, "sha512-yLD5095kVgDw965jepMyUrIgDklD6qH/BZNHeKOgvu7pchOKNjVM+zQoOVYJIKWMWOWBq8IRNVU6NXzBbozaJg=="], + + "@firebase/auth-compat": ["@firebase/auth-compat@0.5.14", "", { "dependencies": { "@firebase/auth": "1.7.9", "@firebase/auth-types": "0.12.2", "@firebase/component": "0.6.9", "@firebase/util": "1.10.0", "tslib": "^2.1.0", "undici": "6.19.7" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-2eczCSqBl1KUPJacZlFpQayvpilg3dxXLy9cSMTKtQMTQSmondUtPI47P3ikH3bQAXhzKLOE+qVxJ3/IRtu9pw=="], + + "@firebase/auth-interop-types": ["@firebase/auth-interop-types@0.2.3", "", {}, "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ=="], + + "@firebase/auth-types": ["@firebase/auth-types@0.12.2", "", { "peerDependencies": { "@firebase/app-types": "0.x", "@firebase/util": "1.x" } }, "sha512-qsEBaRMoGvHO10unlDJhaKSuPn4pyoTtlQuP1ghZfzB6rNQPuhp/N/DcFZxm9i4v0SogjCbf9reWupwIvfmH6w=="], + + "@firebase/component": ["@firebase/component@0.6.9", "", { "dependencies": { "@firebase/util": "1.10.0", "tslib": "^2.1.0" } }, "sha512-gm8EUEJE/fEac86AvHn8Z/QW8BvR56TBw3hMW0O838J/1mThYQXAIQBgUv75EqlCZfdawpWLrKt1uXvp9ciK3Q=="], + + "@firebase/data-connect": ["@firebase/data-connect@0.1.0", "", { "dependencies": { "@firebase/auth-interop-types": "0.2.3", "@firebase/component": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-vSe5s8dY13ilhLnfY0eYRmQsdTbH7PUFZtBbqU6JVX/j8Qp9A6G5gG6//ulbX9/1JFOF1IWNOne9c8S/DOCJaQ=="], + + "@firebase/database": ["@firebase/database@1.0.8", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.2", "@firebase/auth-interop-types": "0.2.3", "@firebase/component": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "faye-websocket": "0.11.4", "tslib": "^2.1.0" } }, "sha512-dzXALZeBI1U5TXt6619cv0+tgEhJiwlUtQ55WNZY7vGAjv7Q1QioV969iYwt1AQQ0ovHnEW0YW9TiBfefLvErg=="], + + "@firebase/database-compat": ["@firebase/database-compat@1.0.8", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/database": "1.0.8", "@firebase/database-types": "1.0.5", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" } }, "sha512-OpeWZoPE3sGIRPBKYnW9wLad25RaWbGyk7fFQe4xnJQKRzlynWeFBSRRAoLE2Old01WXwskUiucNqUUVlFsceg=="], + + "@firebase/database-types": ["@firebase/database-types@1.0.5", "", { "dependencies": { "@firebase/app-types": "0.9.2", "@firebase/util": "1.10.0" } }, "sha512-fTlqCNwFYyq/C6W7AJ5OCuq5CeZuBEsEwptnVxlNPkWCo5cTTyukzAHRSO/jaQcItz33FfYrrFk1SJofcu2AaQ=="], + + "@firebase/firestore": ["@firebase/firestore@4.7.3", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "@firebase/webchannel-wrapper": "1.0.1", "@grpc/grpc-js": "~1.9.0", "@grpc/proto-loader": "^0.7.8", "tslib": "^2.1.0", "undici": "6.19.7" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-NwVU+JPZ/3bhvNSJMCSzfcBZZg8SUGyzZ2T0EW3/bkUeefCyzMISSt/TTIfEHc8cdyXGlMqfGe3/62u9s74UEg=="], + + "@firebase/firestore-compat": ["@firebase/firestore-compat@0.3.38", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/firestore": "4.7.3", "@firebase/firestore-types": "3.0.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-GoS0bIMMkjpLni6StSwRJarpu2+S5m346Na7gr9YZ/BZ/W3/8iHGNr9PxC+f0rNZXqS4fGRn88pICjrZEgbkqQ=="], + + "@firebase/firestore-types": ["@firebase/firestore-types@3.0.2", "", { "peerDependencies": { "@firebase/app-types": "0.x", "@firebase/util": "1.x" } }, "sha512-wp1A+t5rI2Qc/2q7r2ZpjUXkRVPtGMd6zCLsiWurjsQpqPgFin3AhNibKcIzoF2rnToNa/XYtyWXuifjOOwDgg=="], + + "@firebase/functions": ["@firebase/functions@0.11.8", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.2", "@firebase/auth-interop-types": "0.2.3", "@firebase/component": "0.6.9", "@firebase/messaging-interop-types": "0.2.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0", "undici": "6.19.7" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-Lo2rTPDn96naFIlSZKVd1yvRRqqqwiJk7cf9TZhUerwnPKgBzXy+aHE22ry+6EjCaQusUoNai6mU6p+G8QZT1g=="], + + "@firebase/functions-compat": ["@firebase/functions-compat@0.3.14", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/functions": "0.11.8", "@firebase/functions-types": "0.6.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-dZ0PKOKQFnOlMfcim39XzaXonSuPPAVuzpqA4ONTIdyaJK/OnBaIEVs/+BH4faa1a2tLeR+Jy15PKqDRQoNIJw=="], + + "@firebase/functions-types": ["@firebase/functions-types@0.6.2", "", {}, "sha512-0KiJ9lZ28nS2iJJvimpY4nNccV21rkQyor5Iheu/nq8aKXJqtJdeSlZDspjPSBBiHRzo7/GMUttegnsEITqR+w=="], + + "@firebase/installations": ["@firebase/installations@0.6.9", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/util": "1.10.0", "idb": "7.1.1", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-hlT7AwCiKghOX3XizLxXOsTFiFCQnp/oj86zp1UxwDGmyzsyoxtX+UIZyVyH/oBF5+XtblFG9KZzZQ/h+dpy+Q=="], + + "@firebase/installations-compat": ["@firebase/installations-compat@0.2.9", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/installations": "0.6.9", "@firebase/installations-types": "0.5.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-2lfdc6kPXR7WaL4FCQSQUhXcPbI7ol3wF+vkgtU25r77OxPf8F/VmswQ7sgIkBBWtymn5ZF20TIKtnOj9rjb6w=="], + + "@firebase/installations-types": ["@firebase/installations-types@0.5.2", "", { "peerDependencies": { "@firebase/app-types": "0.x" } }, "sha512-que84TqGRZJpJKHBlF2pkvc1YcXrtEDOVGiDjovP/a3s6W4nlbohGXEsBJo0JCeeg/UG9A+DEZVDUV9GpklUzA=="], + + "@firebase/logger": ["@firebase/logger@0.4.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A=="], + + "@firebase/messaging": ["@firebase/messaging@0.12.12", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/installations": "0.6.9", "@firebase/messaging-interop-types": "0.2.2", "@firebase/util": "1.10.0", "idb": "7.1.1", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-6q0pbzYBJhZEtUoQx7hnPhZvAbuMNuBXKQXOx2YlWhSrlv9N1m0ZzlNpBbu/ItTzrwNKTibdYzUyaaxdWLg+4w=="], + + "@firebase/messaging-compat": ["@firebase/messaging-compat@0.2.12", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/messaging": "0.12.12", "@firebase/util": "1.10.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-pKsiUVZrbmRgdImYqhBNZlkKJbqjlPkVdQRZGRbkTyX4OSGKR0F/oJeCt1a8jEg5UnBp4fdVwSWSp4DuCovvEQ=="], + + "@firebase/messaging-interop-types": ["@firebase/messaging-interop-types@0.2.2", "", {}, "sha512-l68HXbuD2PPzDUOFb3aG+nZj5KA3INcPwlocwLZOzPp9rFM9yeuI9YLl6DQfguTX5eAGxO0doTR+rDLDvQb5tA=="], + + "@firebase/performance": ["@firebase/performance@0.6.9", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/installations": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-PnVaak5sqfz5ivhua+HserxTJHtCar/7zM0flCX6NkzBNzJzyzlH4Hs94h2Il0LQB99roBqoE5QT1JqWqcLJHQ=="], + + "@firebase/performance-compat": ["@firebase/performance-compat@0.2.9", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/performance": "0.6.9", "@firebase/performance-types": "0.2.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-dNl95IUnpsu3fAfYBZDCVhXNkASE0uo4HYaEPd2/PKscfTvsgqFAOxfAXzBEDOnynDWiaGUnb5M1O00JQ+3FXA=="], + + "@firebase/performance-types": ["@firebase/performance-types@0.2.2", "", {}, "sha512-gVq0/lAClVH5STrIdKnHnCo2UcPLjJlDUoEB/tB4KM+hAeHUxWKnpT0nemUPvxZ5nbdY/pybeyMe8Cs29gEcHA=="], + + "@firebase/remote-config": ["@firebase/remote-config@0.4.9", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/installations": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-EO1NLCWSPMHdDSRGwZ73kxEEcTopAxX1naqLJFNApp4hO8WfKfmEpmjxmP5TrrnypjIf2tUkYaKsfbEA7+AMmA=="], + + "@firebase/remote-config-compat": ["@firebase/remote-config-compat@0.2.9", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/remote-config": "0.4.9", "@firebase/remote-config-types": "0.3.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-AxzGpWfWFYejH2twxfdOJt5Cfh/ATHONegTd/a0p5flEzsD5JsxXgfkFToop+mypEL3gNwawxrxlZddmDoNxyA=="], + + "@firebase/remote-config-types": ["@firebase/remote-config-types@0.3.2", "", {}, "sha512-0BC4+Ud7y2aPTyhXJTMTFfrGGLqdYXrUB9sJVAB8NiqJswDTc4/2qrE/yfUbnQJhbSi6ZaTTBKyG3n1nplssaA=="], + + "@firebase/storage": ["@firebase/storage@0.13.2", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/util": "1.10.0", "tslib": "^2.1.0", "undici": "6.19.7" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-fxuJnHshbhVwuJ4FuISLu+/76Aby2sh+44ztjF2ppoe0TELIDxPW6/r1KGlWYt//AD0IodDYYA8ZTN89q8YqUw=="], + + "@firebase/storage-compat": ["@firebase/storage-compat@0.3.12", "", { "dependencies": { "@firebase/component": "0.6.9", "@firebase/storage": "0.13.2", "@firebase/storage-types": "0.8.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app-compat": "0.x" } }, "sha512-hA4VWKyGU5bWOll+uwzzhEMMYGu9PlKQc1w4DWxB3aIErWYzonrZjF0icqNQZbwKNIdh8SHjZlFeB2w6OSsjfg=="], + + "@firebase/storage-types": ["@firebase/storage-types@0.8.2", "", { "peerDependencies": { "@firebase/app-types": "0.x", "@firebase/util": "1.x" } }, "sha512-0vWu99rdey0g53lA7IShoA2Lol1jfnPovzLDUBuon65K7uKG9G+L5uO05brD9pMw+l4HRFw23ah3GwTGpEav6g=="], + + "@firebase/util": ["@firebase/util@1.10.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ=="], + + "@firebase/vertexai-preview": ["@firebase/vertexai-preview@0.0.4", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.2", "@firebase/component": "0.6.9", "@firebase/logger": "0.4.2", "@firebase/util": "1.10.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-types": "0.x" } }, "sha512-EBSqyu9eg8frQlVU9/HjKtHN7odqbh9MtAcVz3WwHj4gLCLOoN9F/o+oxlq3CxvFrd3CNTZwu6d2mZtVlEInng=="], + + "@firebase/webchannel-wrapper": ["@firebase/webchannel-wrapper@1.0.1", "", {}, "sha512-jmEnr/pk0yVkA7mIlHNnxCi+wWzOFUg0WyIotgkKAb2u1J7fAeDBcVNSTjTihbAYNusCLQdW5s9IJ5qwnEufcQ=="], + + "@fontsource/roboto": ["@fontsource/roboto@5.2.5", "", {}, "sha512-70r2UZ0raqLn5W+sPeKhqlf8wGvUXFWlofaDlcbt/S3d06+17gXKr3VNqDODB0I1ASme3dGT5OJj9NABt7OTZQ=="], + + "@google-cloud/firestore": ["@google-cloud/firestore@7.11.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0", "fast-deep-equal": "^3.1.1", "functional-red-black-tree": "^1.0.1", "google-gax": "^4.3.3", "protobufjs": "^7.2.6" } }, "sha512-88uZ+jLsp1aVMj7gh3EKYH1aulTAMFAp8sH/v5a9w8q8iqSG27RiWLoxSAFr/XocZ9hGiWH1kEnBw+zl3xAgNA=="], + + "@google-cloud/paginator": ["@google-cloud/paginator@5.0.2", "", { "dependencies": { "arrify": "^2.0.0", "extend": "^3.0.2" } }, "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg=="], + + "@google-cloud/projectify": ["@google-cloud/projectify@4.0.0", "", {}, "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA=="], + + "@google-cloud/promisify": ["@google-cloud/promisify@4.1.0", "", {}, "sha512-G/FQx5cE/+DqBbOpA5jKsegGwdPniU6PuIEMt+qxWgFxvxuFOzVmp6zYchtYuwAWV5/8Dgs0yAmjvNZv3uXLQg=="], + + "@google-cloud/storage": ["@google-cloud/storage@7.15.2", "", { "dependencies": { "@google-cloud/paginator": "^5.0.0", "@google-cloud/projectify": "^4.0.0", "@google-cloud/promisify": "^4.0.0", "abort-controller": "^3.0.0", "async-retry": "^1.3.3", "duplexify": "^4.1.3", "fast-xml-parser": "^4.4.1", "gaxios": "^6.0.2", "google-auth-library": "^9.6.3", "html-entities": "^2.5.2", "mime": "^3.0.0", "p-limit": "^3.0.1", "retry-request": "^7.0.0", "teeny-request": "^9.0.0", "uuid": "^8.0.0" } }, "sha512-+2k+mcQBb9zkaXMllf2wwR/rI07guAx+eZLWsGTDihW2lJRGfiqB7xu1r7/s4uvSP/T+nAumvzT5TTscwHKJ9A=="], + + "@grpc/grpc-js": ["@grpc/grpc-js@1.9.15", "", { "dependencies": { "@grpc/proto-loader": "^0.7.8", "@types/node": ">=12.12.47" } }, "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ=="], + + "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], + + "@hono/zod-validator": ["@hono/zod-validator@0.4.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ=="], + + "@hookform/resolvers": ["@hookform/resolvers@3.10.0", "", { "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + + "@mui/core-downloads-tracker": ["@mui/core-downloads-tracker@5.16.14", "", {}, "sha512-sbjXW+BBSvmzn61XyTMun899E7nGPTXwqD9drm1jBUAvWEhJpPFIRxwQQiATWZnd9rvdxtnhhdsDxEGWI0jxqA=="], + + "@mui/icons-material": ["@mui/icons-material@5.16.14", "", { "dependencies": { "@babel/runtime": "^7.23.9" }, "peerDependencies": { "@mui/material": "^5.0.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-heL4S+EawrP61xMXBm59QH6HODsu0gxtZi5JtnXF2r+rghzyU/3Uftlt1ij8rmJh+cFdKTQug1L9KkZB5JgpMQ=="], + + "@mui/material": ["@mui/material@5.16.14", "", { "dependencies": { "@babel/runtime": "^7.23.9", "@mui/core-downloads-tracker": "^5.16.14", "@mui/system": "^5.16.14", "@mui/types": "^7.2.15", "@mui/utils": "^5.16.14", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.10", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1", "react-is": "^19.0.0", "react-transition-group": "^4.4.5" }, "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/react", "@emotion/styled", "@types/react"] }, "sha512-eSXQVCMKU2xc7EcTxe/X/rC9QsV2jUe8eLM3MUCPYbo6V52eCE436akRIvELq/AqZpxx2bwkq7HC0cRhLB+yaw=="], + + "@mui/private-theming": ["@mui/private-theming@5.16.14", "", { "dependencies": { "@babel/runtime": "^7.23.9", "@mui/utils": "^5.16.14", "prop-types": "^15.8.1" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-12t7NKzvYi819IO5IapW2BcR33wP/KAVrU8d7gLhGHoAmhDxyXlRoKiRij3TOD8+uzk0B6R9wHUNKi4baJcRNg=="], + + "@mui/styled-engine": ["@mui/styled-engine@5.16.14", "", { "dependencies": { "@babel/runtime": "^7.23.9", "@emotion/cache": "^11.13.5", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "peerDependencies": { "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/react", "@emotion/styled"] }, "sha512-UAiMPZABZ7p8mUW4akDV6O7N3+4DatStpXMZwPlt+H/dA0lt67qawN021MNND+4QTpjaiMYxbhKZeQcyWCbuKw=="], + + "@mui/system": ["@mui/system@5.16.14", "", { "dependencies": { "@babel/runtime": "^7.23.9", "@mui/private-theming": "^5.16.14", "@mui/styled-engine": "^5.16.14", "@mui/types": "^7.2.15", "@mui/utils": "^5.16.14", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/react", "@emotion/styled", "@types/react"] }, "sha512-KBxMwCb8mSIABnKvoGbvM33XHyT+sN0BzEBG+rsSc0lLQGzs7127KWkCA6/H8h6LZ00XpBEME5MAj8mZLiQ1tw=="], + + "@mui/types": ["@mui/types@7.2.21", "", { "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww=="], + + "@mui/utils": ["@mui/utils@5.16.14", "", { "dependencies": { "@babel/runtime": "^7.23.9", "@mui/types": "^7.2.15", "@types/prop-types": "^15.7.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-is": "^19.0.0" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-wn1QZkRzSmeXD1IguBVvJJHV3s6rxJrfb6YuC9Kk6Noh9f8Fb54nUs5JRkKm+BOerRhj5fLg05Dhx/H3Ofb8Mg=="], + + "@next/env": ["@next/env@14.2.24", "", {}, "sha512-LAm0Is2KHTNT6IT16lxT+suD0u+VVfYNQqM+EJTKuFRRuY2z+zj01kueWXPCxbMBDt0B5vONYzabHGUNbZYAhA=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.2.24", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7Tdi13aojnAZGpapVU6meVSpNzgrFwZ8joDcNS8cJVNuP3zqqrLqeory9Xec5TJZR/stsGJdfwo8KeyloT3+rQ=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.2.24", "", { "os": "darwin", "cpu": "x64" }, "sha512-lXR2WQqUtu69l5JMdTwSvQUkdqAhEWOqJEYUQ21QczQsAlNOW2kWZCucA6b3EXmPbcvmHB1kSZDua/713d52xg=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.2.24", "", { "os": "linux", "cpu": "arm64" }, "sha512-nxvJgWOpSNmzidYvvGDfXwxkijb6hL9+cjZx1PVG6urr2h2jUqBALkKjT7kpfurRWicK6hFOvarmaWsINT1hnA=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.2.24", "", { "os": "linux", "cpu": "arm64" }, "sha512-PaBgOPhqa4Abxa3y/P92F3kklNPsiFjcjldQGT7kFmiY5nuFn8ClBEoX8GIpqU1ODP2y8P6hio6vTomx2Vy0UQ=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.2.24", "", { "os": "linux", "cpu": "x64" }, "sha512-vEbyadiRI7GOr94hd2AB15LFVgcJZQWu7Cdi9cWjCMeCiUsHWA0U5BkGPuoYRnTxTn0HacuMb9NeAmStfBCLoQ=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.2.24", "", { "os": "linux", "cpu": "x64" }, "sha512-df0FC9ptaYsd8nQCINCzFtDWtko8PNRTAU0/+d7hy47E0oC17tI54U/0NdGk7l/76jz1J377dvRjmt6IUdkpzQ=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.2.24", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZEntbLjeYAJ286eAqbxpZHhDFYpYjArotQ+/TW9j7UROh0DUmX7wYDGtsTPpfCV8V+UoqHBPU7q9D4nDNH014Q=="], + + "@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.24", "", { "os": "win32", "cpu": "ia32" }, "sha512-9KuS+XUXM3T6v7leeWU0erpJ6NsFIwiTFD5nzNg8J5uo/DMIPvCp3L1Ao5HjbHX0gkWPB1VrKoo/Il4F0cGK2Q=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.2.24", "", { "os": "win32", "cpu": "x64" }, "sha512-cXcJ2+x0fXQ2CntaE00d7uUH+u1Bfp/E0HsNQH79YiLaZE5Rbm7dZzyAYccn3uICM7mw+DxoMqEfGXZtF4Fgaw=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="], + + "@prisma/client": ["@prisma/client@5.22.0", "", { "peerDependencies": { "prisma": "*" }, "optionalPeers": ["prisma"] }, "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA=="], + + "@prisma/debug": ["@prisma/debug@5.22.0", "", {}, "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ=="], + + "@prisma/engines": ["@prisma/engines@5.22.0", "", { "dependencies": { "@prisma/debug": "5.22.0", "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", "@prisma/fetch-engine": "5.22.0", "@prisma/get-platform": "5.22.0" } }, "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA=="], + + "@prisma/engines-version": ["@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", "", {}, "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ=="], + + "@prisma/fetch-engine": ["@prisma/fetch-engine@5.22.0", "", { "dependencies": { "@prisma/debug": "5.22.0", "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", "@prisma/get-platform": "5.22.0" } }, "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA=="], + + "@prisma/get-platform": ["@prisma/get-platform@5.22.0", "", { "dependencies": { "@prisma/debug": "5.22.0" } }, "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + + "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + + "@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="], + + "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], + + "@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="], + + "@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="], + + "@types/caseless": ["@types/caseless@0.12.5", "", {}, "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/cookie-parser": ["@types/cookie-parser@1.4.8", "", { "peerDependencies": { "@types/express": "*" } }, "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ=="], + + "@types/cors": ["@types/cors@2.8.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA=="], + + "@types/css-modules": ["@types/css-modules@1.0.5", "", {}, "sha512-oeKafs/df9lwOvtfiXVliZsocFVOexK9PZtLQWuPeuVCFR7jwiqlg60lu80JTe5NFNtH3tnV6Fs/ySR8BUPHAw=="], + + "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], + + "@types/express": ["@types/express@4.17.21", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], + + "@types/http-errors": ["@types/http-errors@2.0.4", "", {}, "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.9", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ=="], + + "@types/long": ["@types/long@4.0.2", "", {}, "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="], + + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + + "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], + + "@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="], + + "@types/qs": ["@types/qs@6.9.18", "", {}, "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/react": ["@types/react@18.3.18", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ=="], + + "@types/react-dom": ["@types/react-dom@18.3.5", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q=="], + + "@types/react-transition-group": ["@types/react-transition-group@4.4.12", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="], + + "@types/request": ["@types/request@2.48.12", "", { "dependencies": { "@types/caseless": "*", "@types/node": "*", "@types/tough-cookie": "*", "form-data": "^2.5.0" } }, "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw=="], + + "@types/send": ["@types/send@0.17.4", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA=="], + + "@types/serve-static": ["@types/serve-static@1.15.7", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw=="], + + "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], + + "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + + "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], + + "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-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], + + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "arrify": ["arrify@2.0.1", "", {}, "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="], + + "async-retry": ["async-retry@1.3.3", "", { "dependencies": { "retry": "0.13.1" } }, "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "autoprefixer": ["autoprefixer@10.4.20", "", { "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g=="], + + "babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bignumber.js": ["bignumber.js@9.1.2", "", {}, "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + + "bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="], + + "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001702", "", {}, "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA=="], + + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], + + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + + "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=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + + "common": ["common@workspace:common"], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-parser": ["cookie-parser@1.4.7", "", { "dependencies": { "cookie": "0.7.2", "cookie-signature": "1.0.6" } }, "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw=="], + + "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], + + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + + "cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "css-selector-tokenizer": ["css-selector-tokenizer@0.8.0", "", { "dependencies": { "cssesc": "^3.0.0", "fastparse": "^1.1.2" } }, "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "culori": ["culori@3.3.0", "", {}, "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ=="], + + "daisyui": ["daisyui@4.12.24", "", { "dependencies": { "css-selector-tokenizer": "^0.8", "culori": "^3", "picocolors": "^1", "postcss-js": "^4" } }, "sha512-JYg9fhQHOfXyLadrBrEqCDM6D5dWCSSiM6eTNCRrBRzx/VlOCrLS8eDfIw9RVvs64v2mJdLooKXY8EwQzoszAA=="], + + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="], + + "devalue": ["devalue@5.1.1", "", {}, "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="], + + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + + "discontinuous-range": ["discontinuous-range@1.0.0", "", {}, "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], + + "dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="], + + "dotenv-cli": ["dotenv-cli@7.4.4", "", { "dependencies": { "cross-spawn": "^7.0.6", "dotenv": "^16.3.0", "dotenv-expand": "^10.0.0", "minimist": "^1.2.6" }, "bin": { "dotenv": "cli.js" } }, "sha512-XkBYCG0tPIes+YZr4SpfFv76SQrV/LeCE8CI7JSEMi3VR9MvTihCGTOtbIexD6i2mXF+6px7trb1imVCXSNMDw=="], + + "dotenv-expand": ["dotenv-expand@10.0.0", "", {}, "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.112", "", {}, "sha512-oen93kVyqSb3l+ziUgzIOlWt/oOuy4zRmpwestMn4rhFWAoFJeFuCVte9F2fASjeZZo7l/Cif9TiyrdW4CwEMA=="], + + "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], + + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + + "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.21.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.0", "@eslint/js": "9.21.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "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.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.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-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg=="], + + "eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], + + "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "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=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "farmhash-modern": ["farmhash-modern@1.1.0", "", {}, "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "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=="], + + "fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="], + + "fastparse": ["fastparse@1.1.2", "", {}, "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "faye-websocket": ["faye-websocket@0.11.4", "", { "dependencies": { "websocket-driver": ">=0.5.1" } }, "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "firebase": ["firebase@10.14.1", "", { "dependencies": { "@firebase/analytics": "0.10.8", "@firebase/analytics-compat": "0.2.14", "@firebase/app": "0.10.13", "@firebase/app-check": "0.8.8", "@firebase/app-check-compat": "0.3.15", "@firebase/app-compat": "0.2.43", "@firebase/app-types": "0.9.2", "@firebase/auth": "1.7.9", "@firebase/auth-compat": "0.5.14", "@firebase/data-connect": "0.1.0", "@firebase/database": "1.0.8", "@firebase/database-compat": "1.0.8", "@firebase/firestore": "4.7.3", "@firebase/firestore-compat": "0.3.38", "@firebase/functions": "0.11.8", "@firebase/functions-compat": "0.3.14", "@firebase/installations": "0.6.9", "@firebase/installations-compat": "0.2.9", "@firebase/messaging": "0.12.12", "@firebase/messaging-compat": "0.2.12", "@firebase/performance": "0.6.9", "@firebase/performance-compat": "0.2.9", "@firebase/remote-config": "0.4.9", "@firebase/remote-config-compat": "0.2.9", "@firebase/storage": "0.13.2", "@firebase/storage-compat": "0.3.12", "@firebase/util": "1.10.0", "@firebase/vertexai-preview": "0.0.4" } }, "sha512-0KZxU+Ela9rUCULqFsUUOYYkjh7OM1EWdIfG6///MtXd0t2/uUIf0iNV5i0KariMhRQ5jve/OY985nrAXFaZeQ=="], + + "firebase-admin": ["firebase-admin@12.7.0", "", { "dependencies": { "@fastify/busboy": "^3.0.0", "@firebase/database-compat": "1.0.8", "@firebase/database-types": "1.0.5", "@types/node": "^22.0.1", "farmhash-modern": "^1.1.0", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.1.0", "node-forge": "^1.3.1", "uuid": "^10.0.0" }, "optionalDependencies": { "@google-cloud/firestore": "^7.7.0", "@google-cloud/storage": "^7.7.0" } }, "sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA=="], + + "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=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "form-data": ["form-data@2.5.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ=="], + + "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], + + "framer-motion": ["framer-motion@11.18.2", "", { "dependencies": { "motion-dom": "^11.18.1", "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "functional-red-black-tree": ["functional-red-black-tree@1.0.1", "", {}, "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g=="], + + "gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="], + + "gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stdin": ["get-stdin@8.0.0", "", {}, "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg=="], + + "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + + "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], + + "goober": ["goober@2.1.16", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g=="], + + "google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="], + + "google-gax": ["google-gax@4.4.1", "", { "dependencies": { "@grpc/grpc-js": "^1.10.9", "@grpc/proto-loader": "^0.7.13", "@types/long": "^4.0.0", "abort-controller": "^3.0.0", "duplexify": "^4.0.0", "google-auth-library": "^9.3.0", "node-fetch": "^2.7.0", "object-hash": "^3.0.0", "proto3-json-serializer": "^2.0.2", "protobufjs": "^7.3.2", "retry-request": "^7.0.0", "uuid": "^9.0.1" } }, "sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg=="], + + "google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], + + "hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="], + + "html-entities": ["html-entities@2.5.2", "", {}, "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA=="], + + "http-parser-js": ["http-parser-js@0.5.9", "", {}, "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw=="], + + "http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + + "idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="], + + "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=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "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-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "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=="], + + "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], + + "jwa": ["jwa@1.4.1", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA=="], + + "jwks-rsa": ["jwks-rsa@3.1.0", "", { "dependencies": { "@types/express": "^4.17.17", "@types/jsonwebtoken": "^9.0.2", "debug": "^4.3.4", "jose": "^4.14.6", "limiter": "^1.1.5", "lru-memoizer": "^2.2.0" } }, "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg=="], + + "jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "lefthook": ["lefthook@1.11.2", "", { "optionalDependencies": { "lefthook-darwin-arm64": "1.11.2", "lefthook-darwin-x64": "1.11.2", "lefthook-freebsd-arm64": "1.11.2", "lefthook-freebsd-x64": "1.11.2", "lefthook-linux-arm64": "1.11.2", "lefthook-linux-x64": "1.11.2", "lefthook-openbsd-arm64": "1.11.2", "lefthook-openbsd-x64": "1.11.2", "lefthook-windows-arm64": "1.11.2", "lefthook-windows-x64": "1.11.2" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-/5royc/WbL2KTfFJ54wEdvxUZOBXwc54v/fW2Bz4LMOkAA3LWIxnoUiybSiauu+nhdTG98qERxH1YHwF2wZlAA=="], + + "lefthook-darwin-arm64": ["lefthook-darwin-arm64@1.11.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8DpvrybtWdt6UmfZk+hA8daYXr6zkpJVogZ8M49BQx6ISSKUaC03xzO1m4MrAsoKok77ka4JAidYhOa2gCu15A=="], + + "lefthook-darwin-x64": ["lefthook-darwin-x64@1.11.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-DrL1SOT8lJksjudRu6fTZTp3M0EbpCP2RQ22MDT71clS8BMrFL8x3h9Ziw+uNH76j9zA241tW5zMxWMSv+foAA=="], + + "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@1.11.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-AliG4Wi8BNC27hCSnuFBeUXh/eA3fppnUbQQPISy/G94yfwRkzyml9MZzvb7HKmUpw1LT0sq9RQ6FQPxBZ2DYA=="], + + "lefthook-freebsd-x64": ["lefthook-freebsd-x64@1.11.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-V6cgRCoi5+jcq6XBIdRYraeEOK1UhBrtL/XZlNypAIkhPoBtfTP9u2wSprGMDzZvJCRriLXZxV/d0v94laKXzA=="], + + "lefthook-linux-arm64": ["lefthook-linux-arm64@1.11.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-VKcK7sjIK8UpXX/qK6Fxa0Lnwr4gzRtlXDS17jzxThcyFk8iGBpQ+9ZnPLv2yAaEIzmGhJUG9sDgOb9IQ5kpBQ=="], + + "lefthook-linux-x64": ["lefthook-linux-x64@1.11.2", "", { "os": "linux", "cpu": "x64" }, "sha512-aGa2Krph14YwSW7KF0PrlCBK9P7V/Z4oFklonmz3r2Fjm8EdhA750y7OQvA9KerXRleIb5SaUH/cz1azG/izeQ=="], + + "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@1.11.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-f7owNQ9Ki6Y07KBgdXdH28EYO0eBdZuGTpIggMeHNhYFVDavxuINP2BjmbXtzpUu8K5BX6exGx0umtWhRhXbvQ=="], + + "lefthook-openbsd-x64": ["lefthook-openbsd-x64@1.11.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HKv6PV64vOjqPrlxAqo07N9+Z34jdPDBfeExqi0ldR7vACFaBJFIdhWCLLP+3uQUrNKc8GXlikqplZn8MgRSQw=="], + + "lefthook-windows-arm64": ["lefthook-windows-arm64@1.11.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-042jCKZ/H+lS6XYoMIf2FWMP2hxXqfAT52UW6lYObIOvQ5xu/epUXFjtmXRyYxCv57No3JYYMg1Yr06xdzTKkQ=="], + + "lefthook-windows-x64": ["lefthook-windows-x64@1.11.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1Map6Ck2AyfY6ptN9T19N41HFKFqRTzmILtGaRGJABEzHiE4+gSWcq5YT1R6cCtkVlewD3Lx+J/80D/Kb/cVtw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "limiter": ["limiter@1.1.5", "", {}, "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "lint-staged": ["lint-staged@15.4.3", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^13.1.0", "debug": "^4.4.0", "execa": "^8.0.1", "lilconfig": "^3.1.3", "listr2": "^8.2.5", "micromatch": "^4.0.8", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.7.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g=="], + + "listr2": ["listr2@8.2.5", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + + "lodash.clonedeep": ["lodash.clonedeep@4.5.0", "", {}, "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="], + + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], + + "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + + "long": ["long@5.3.1", "", {}, "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lru-cache": ["lru-cache@11.0.2", "", {}, "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA=="], + + "lru-memoizer": ["lru-memoizer@2.3.0", "", { "dependencies": { "lodash.clonedeep": "^4.5.0", "lru-cache": "6.0.0" } }, "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "moo": ["moo@0.5.2", "", {}, "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q=="], + + "motion-dom": ["motion-dom@11.18.1", "", { "dependencies": { "motion-utils": "^11.18.1" } }, "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw=="], + + "motion-utils": ["motion-utils@11.18.1", "", {}, "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "nearley": ["nearley@2.20.1", "", { "dependencies": { "commander": "^2.19.0", "moo": "^0.5.0", "railroad-diagrams": "^1.0.0", "randexp": "0.4.6" }, "bin": { "nearleyc": "bin/nearleyc.js", "nearley-test": "bin/nearley-test.js", "nearley-unparse": "bin/nearley-unparse.js", "nearley-railroad": "bin/nearley-railroad.js" } }, "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ=="], + + "next": ["next@14.2.24", "", { "dependencies": { "@next/env": "14.2.24", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", "styled-jsx": "5.1.1" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.2.24", "@next/swc-darwin-x64": "14.2.24", "@next/swc-linux-arm64-gnu": "14.2.24", "@next/swc-linux-arm64-musl": "14.2.24", "@next/swc-linux-x64-gnu": "14.2.24", "@next/swc-linux-x64-musl": "14.2.24", "@next/swc-win32-arm64-msvc": "14.2.24", "@next/swc-win32-ia32-msvc": "14.2.24", "@next/swc-win32-x64-msvc": "14.2.24" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-En8VEexSJ0Py2FfVnRRh8gtERwDRaJGNvsvad47ShkC2Yi8AXQPXEA2vKoDJlGFSj5WE5SyF21zNi4M5gyi+SQ=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-forge": ["node-forge@1.3.1", "", {}, "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="], + + "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], + + "normalize-wheel": ["normalize-wheel@1.0.1", "", {}, "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA=="], + + "notistack": ["notistack@3.0.2", "", { "dependencies": { "clsx": "^1.1.0", "goober": "^2.0.33" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-0R+/arLYbK5Hh7mEfR2adt0tyXJcCC9KkA2hc56FeWik2QN6Bm/S4uW+BjzDARsJth5u06nTjelSw/VSnB1YEA=="], + + "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], + + "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=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "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=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], + + "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="], + + "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], + + "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], + + "postcss-js": ["postcss-js@4.0.1", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw=="], + + "postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prisma": ["prisma@5.22.0", "", { "dependencies": { "@prisma/engines": "5.22.0" }, "optionalDependencies": { "fsevents": "2.3.3" }, "bin": { "prisma": "build/index.js" } }, "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "proto3-json-serializer": ["proto3-json-serializer@2.0.2", "", { "dependencies": { "protobufjs": "^7.2.5" } }, "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ=="], + + "protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "railroad-diagrams": ["railroad-diagrams@1.0.0", "", {}, "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A=="], + + "randexp": ["randexp@0.4.6", "", { "dependencies": { "discontinuous-range": "1.0.0", "ret": "~0.1.10" } }, "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ=="], + + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + + "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + + "react-easy-crop": ["react-easy-crop@5.4.1", "", { "dependencies": { "normalize-wheel": "^1.0.1", "tslib": "^2.0.1" }, "peerDependencies": { "react": ">=16.4.0", "react-dom": ">=16.4.0" } }, "sha512-Djtsi7bWO75vkKYkVxNRrJWY69pXLahIAkUN0mmt9cXNnaq2tpG59ctSY6P7ipJgBc7COJDRMRuwb2lYwtACNQ=="], + + "react-hook-form": ["react-hook-form@7.54.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg=="], + + "react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="], + + "react-is": ["react-is@19.0.0", "", {}, "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g=="], + + "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="], + + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "ret": ["ret@0.1.15", "", {}, "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="], + + "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + + "retry-request": ["retry-request@7.0.2", "", { "dependencies": { "@types/request": "^2.48.8", "extend": "^3.0.2", "teeny-request": "^9.0.0" } }, "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + + "server": ["server@workspace:server"], + + "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + + "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=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + + "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], + + "source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "sql-formatter": ["sql-formatter@15.4.11", "", { "dependencies": { "argparse": "^2.0.1", "get-stdin": "=8.0.0", "nearley": "^2.20.1" }, "bin": { "sql-formatter": "bin/sql-formatter-cli.cjs" } }, "sha512-AfIjH0mYxv0NVzs4mbcGIAcos2Si20LeF9GMk0VmVA4A3gs1PFIixVu3rtcz34ls7ghPAjrDb+XbRly/aF6HAg=="], + + "stream-events": ["stream-events@1.0.5", "", { "dependencies": { "stubs": "^3.0.0" } }, "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg=="], + + "stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="], + + "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], + + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], + + "stubs": ["stubs@3.0.0", "", {}, "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw=="], + + "styled-jsx": ["styled-jsx@5.1.1", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" } }, "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw=="], + + "stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], + + "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], + + "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=="], + + "tailwindcss": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="], + + "teeny-request": ["teeny-request@9.0.0", "", { "dependencies": { "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.9", "stream-events": "^1.0.5", "uuid": "^9.0.0" } }, "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "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.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + + "undici": ["undici@6.19.7", "", {}, "sha512-HR3W/bMGPSr90i8AAp2C4DM3wChFdJPLrWYpIS++LxS8K+W535qftjt+4MyjNYHeWabMj1nvtmLIi7l++iq91A=="], + + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], + + "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@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "web": ["web@workspace:web"], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "websocket-driver": ["websocket-driver@0.7.4", "", { "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg=="], + + "websocket-extensions": ["websocket-extensions@0.1.4", "", {}, "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], + + "@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + + "@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=="], + + "@google-cloud/storage/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "cosmiconfig/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + + "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "gaxios/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + + "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "google-auth-library/jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="], + + "google-gax/@grpc/grpc-js": ["@grpc/grpc-js@1.12.6", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q=="], + + "google-gax/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + + "gtoken/jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="], + + "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "log-update/slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], + + "lru-memoizer/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "nearley/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + + "notistack/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "teeny-request/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + + "teeny-request/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "google-auth-library/jws/jwa": ["jwa@2.0.0", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA=="], + + "gtoken/jws/jwa": ["jwa@2.0.0", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA=="], + + "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="], + + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "teeny-request/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + } +} diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 6a861def..00000000 Binary files a/bun.lockb and /dev/null differ diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 00000000..7868d6b1 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[install] +saveTextLockfile = true diff --git a/common/README.md b/common/README.md new file mode 100644 index 00000000..2bfd5844 --- /dev/null +++ b/common/README.md @@ -0,0 +1,15 @@ +# common + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.1.29. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/common/consts.ts b/common/consts.ts index 17098809..bb92cc12 100644 --- a/common/consts.ts +++ b/common/consts.ts @@ -10,4 +10,20 @@ export const DAY_TO_JAPANESE_MAP = new Map([ ["sun", "日"], ]); -export const ACTIVE_DAYS = ["mon", "tue", "wed", "thu", "fri", "sat"] as const; +export const ACTIVE_DAYS = ["mon", "tue", "wed", "thu", "fri"] as const; + +export const sortSlots = ( + slots: { + day: "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun" | "other"; + period: number; + }[], +) => { + const order = ["mon", "tue", "wed", "thu", "fri", "sat", "sun", "other"]; + return slots.sort((a, b) => { + const dayComparison = order.indexOf(a.day) - order.indexOf(b.day); + if (dayComparison !== 0) { + return dayComparison; + } + return a.period - b.period; + }); +}; diff --git a/common/lib/panic.ts b/common/lib/panic.ts new file mode 100644 index 00000000..0a7ae204 --- /dev/null +++ b/common/lib/panic.ts @@ -0,0 +1,6 @@ +// unexpected error +export function panic(reason: string): never { + throw new Error(reason, { + cause: "panic", + }); +} diff --git a/common/lib/result.ts b/common/lib/result.ts deleted file mode 100644 index e7d7e369..00000000 --- a/common/lib/result.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** -# Result -Result allows you to handle results in safer and more type-safe way. -Result を使うと、失敗する可能性がある関数の結果をより安全で Type-safe な方法で扱うことができます。 - -basic use case: -```js -function fallible() { - if (Math.random() > 0.5) - throw new Error("something went wrong!"); - return "ok" -} - -const result = safeTry(() => fallible()); -if (!result.ok) { - // handle error - console.error(result.error); - return; -} - -// use value without worrying about thrown errors -console.log(result.value); // -> ok -``` - -or better, make it built in to your function. - -```js -// never throws. -function safeFallible(): Result { - if (Math.random() > 0.5) - return Err("something went wrong!"); - return Ok("ok"); -} - -const result = safeFallible(); -if (!result.ok) { - // handle error - console.error(result.error); - return; -} - -console.log(result.value); -``` -**/ - -// Core - -export type Result = Ok | Err; - -type Ok = { - ok: true; - value: T; -}; - -type Err = { - ok: false; - error: unknown; -}; - -// Core functions - -export function Ok(value: T): Ok { - return { - ok: true, - value, - }; -} - -export function Err(error: unknown): Err { - return { - ok: false, - error, - }; -} - -// Utility functions - -export function safeTry(fallible: () => T): Result { - try { - return Ok(fallible()); - } catch (e) { - return Err(e); - } -} - -export async function safeTryAsync( - fallible: () => Promise, -): Promise> { - try { - return Ok(await fallible()); - } catch (e) { - return Err(e); - } -} - -export function safify(fallible: (v: T) => U): (v: T) => Result { - return (v: T): Result => { - try { - return Ok(fallible(v)); - } catch (e) { - return Err(e); - } - }; -} - -export function safifyAsync( - fallible: (v: T) => Promise, -): (v: T) => Promise> { - return async (v: T): Promise> => { - try { - return Ok(await fallible(v)); - } catch (e) { - return Err(e); - } - }; -} diff --git a/common/lib/result/safeParseInt.ts b/common/lib/result/safeParseInt.ts deleted file mode 100644 index b9705598..00000000 --- a/common/lib/result/safeParseInt.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Err, Ok, type Result } from "../result"; - -export function safeParseInt(s: string | undefined): Result { - if (!s) return Err(new Error("empty string")); - const n = Number.parseInt(s); - if (Number.isNaN(n)) return Err(new Error(`invalid formatting: ${s}`)); - return Ok(n); -} diff --git a/common/package.json b/common/package.json new file mode 100644 index 00000000..46c6de25 --- /dev/null +++ b/common/package.json @@ -0,0 +1,14 @@ +{ + "name": "common", + "private": true, + "version": "1.0.0", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0", + "zod": "^3.23.8" + }, + "dependencies": {} +} diff --git a/common/tsconfig.json b/common/tsconfig.json new file mode 100644 index 00000000..238655f2 --- /dev/null +++ b/common/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "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, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/common/types.ts b/common/types.ts index 0bd9ed92..7bce202d 100644 --- a/common/types.ts +++ b/common/types.ts @@ -1,21 +1,27 @@ // common/type/types.ts +import type { Message } from "./zod/types.ts"; + export type { UserID, GUID, IDToken, Gender, RelationshipStatus, + InterestSubject, + Interest, User, InitUser, UpdateUser, RelationshipID, Relationship, CourseID, + InterestSubjectID, Slot, Course, Enrollment, Day, + UserWithCoursesAndSubjects, MessageID, ShareRoomID, Message, @@ -30,3 +36,23 @@ export type { InitSharedRoom, UpdateRoom, } from "./zod/types.ts"; + +export type SSEChatEvents = { + "Chat:Append": { + message: Message; + sender: string; // user name + }; + "Chat:Update": { + id: number; + message: Message; + }; + "Chat:Delete": { + id: number; + }; + "Chat:Ping": ""; +}; +export type SSEChatEventEnum = "Chat:Append" | "Chat:Update" | "Chat:Delete"; +export type SSEChatEvent = { + event: T; + data: SSEChatEvents[T]; +}; diff --git a/common/zod/schemas.ts b/common/zod/schemas.ts index 2e8d8c0b..0ac20696 100644 --- a/common/zod/schemas.ts +++ b/common/zod/schemas.ts @@ -65,6 +65,8 @@ export const RelationshipSchema = z.object({ export const CourseIDSchema = z.string(); +export const InterestSubjectIDSchema = z.number(); + export const DaySchema = z.enum([ "mon", "tue", @@ -76,7 +78,7 @@ export const DaySchema = z.enum([ "other", ]); -export const PeriodSchema = z.number().min(0).max(6); +export const PeriodSchema = z.coerce.number().min(0).max(6); export const SlotSchema = z.object({ day: DaySchema, @@ -97,6 +99,22 @@ export const EnrollmentSchema = z.object({ courseId: CourseIDSchema, }); +export const InterestSubjectSchema = z.object({ + id: InterestSubjectIDSchema, + name: z.string(), + group: z.string(), +}); + +export const InterestSchema = z.object({ + userId: UserIDSchema, + subjectId: InterestSubjectIDSchema, +}); + +export const UserWithCoursesAndSubjectsSchema = UserSchema.extend({ + courses: CourseSchema.array(), + interestSubjects: InterestSubjectSchema.array(), +}); + export const MessageIDSchema = z.number(); // TODO! Add __internal_prevent_cast_MessageID: PhantomData export const ShareRoomIDSchema = z.number(); @@ -110,6 +128,7 @@ export const MessageSchema = z.object({ creator: UserIDSchema, createdAt: z.date(), content: ContentSchema, + isPicture: z.boolean(), edited: z.boolean(), }); @@ -117,12 +136,20 @@ export const SendMessageSchema = z.object({ content: z.string().min(1, { message: "Content must not be empty." }), }); +export const MatchingStatusSchema = z.union([ + z.literal("myRequest"), + z.literal("otherRequest"), + z.literal("matched"), +]); + export const DMOverviewSchema = z.object({ isDM: z.literal(true), + matchingStatus: MatchingStatusSchema, friendId: UserIDSchema, name: NameSchema, thumbnail: z.string(), lastMsg: MessageSchema.optional(), + unreadMessages: z.number(), }); export const SharedRoomOverviewSchema = z.object({ @@ -147,6 +174,9 @@ export const DMRoomSchema = z.object({ export const PersonalizedDMRoomSchema = z.object({ name: NameSchema, thumbnail: z.string(), + matchingStatus: MatchingStatusSchema, + unreadMessages: z.number(), + friendId: z.number(), }); export const SharedRoomSchema = z.object({ diff --git a/common/zod/types.ts b/common/zod/types.ts index 6929b022..cb195e8b 100644 --- a/common/zod/types.ts +++ b/common/zod/types.ts @@ -14,6 +14,9 @@ import type { InitRoomSchema, InitSharedRoomSchema, InitUserSchema, + InterestSchema, + InterestSubjectIDSchema, + InterestSubjectSchema, IntroLongSchema, IntroShortSchema, MessageIDSchema, @@ -36,6 +39,7 @@ import type { UpdateUserSchema, UserIDSchema, UserSchema, + UserWithCoursesAndSubjectsSchema, } from "./schemas"; export type UserID = z.infer; @@ -45,6 +49,8 @@ export type Name = z.infer; export type PictureUrl = z.infer; export type Gender = z.infer; export type RelationshipStatus = z.infer; +export type InterestSubject = z.infer; +export type Interest = z.infer; export type User = z.infer; export type InitUser = z.infer; export type UpdateUser = z.infer; @@ -52,11 +58,15 @@ export type Step1User = z.infer; export type RelationshipID = z.infer; export type Relationship = z.infer; export type CourseID = z.infer; +export type InterestSubjectID = z.infer; export type Slot = z.infer; export type Course = z.infer; export type Enrollment = z.infer; export type Day = z.infer; export type Period = z.infer; +export type UserWithCoursesAndSubjects = z.infer< + typeof UserWithCoursesAndSubjectsSchema +>; export type MessageID = z.infer; export type ShareRoomID = z.infer; export type Message = z.infer; diff --git a/flake.lock b/flake.lock index 6ccba3b6..8b9b19c0 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1726560853, - "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -23,11 +23,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -38,27 +38,27 @@ }, "nixpkgs": { "locked": { - "lastModified": 1727509270, - "narHash": "sha256-WD2APik+VEDBHaYswnhP+lOUKFXgQ2kEQTbYfgXGqWw=", + "lastModified": 1739262174, + "narHash": "sha256-W0436s/nXInSuxfiXKtT2n1nUkNw8Ibddz7w4GAweJ4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f7d490f88874e5ffd89a5bb99c5381406958b2ef", + "rev": "2a39e74c3ea50a164168215a47b86f72180db76c", "type": "github" }, "original": { "owner": "NixOS", - "ref": "master", + "ref": "release-24.11", "repo": "nixpkgs", "type": "github" } }, - "pkgs": { + "nixpkgs-unstable": { "locked": { - "lastModified": 1718870667, - "narHash": "sha256-jab3Kpc8O1z3qxwVsCMHL4+18n5Wy/HHKyu1fcsF7gs=", + "lastModified": 1739019272, + "narHash": "sha256-7Fu7oazPoYCbDzb9k8D/DdbKrC3aU1zlnc39Y8jy/s8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9b10b8f00cb5494795e5f51b39210fed4d2b0748", + "rev": "fa35a3c8e17a3de613240fea68f876e5b4896aec", "type": "github" }, "original": { @@ -71,14 +71,17 @@ "prisma-utils": { "inputs": { "flake-utils": "flake-utils_2", - "pkgs": "pkgs" + "nixpkgs": [ + "nixpkgs" + ], + "treefmt-nix": "treefmt-nix" }, "locked": { - "lastModified": 1719301942, - "narHash": "sha256-qKvcOVKoZqCfkR7Mt+7LCrsXMCNysY6JSy6QY/vXP4g=", + "lastModified": 1738796520, + "narHash": "sha256-A/hOhcE/16QlknM7XswWmj3I+XQob6Cclfm4AgoLIPE=", "owner": "VanCoding", "repo": "nix-prisma-utils", - "rev": "3bf92853b6986daca112b7e07fc33f976f48ffe7", + "rev": "0d4f9a4e3efcfa3161f2614c4964b5b34fac4298", "type": "github" }, "original": { @@ -91,7 +94,29 @@ "inputs": { "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", - "prisma-utils": "prisma-utils" + "nixpkgs-unstable": "nixpkgs-unstable", + "prisma-utils": "prisma-utils", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1739240901, + "narHash": "sha256-YDtl/9w71m5WcZvbEroYoWrjECDhzJZLZ8E68S3BYok=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "03473e2af8a4b490f4d2cdb2e4d3b75f82c8197c", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" } }, "systems": { @@ -123,6 +148,27 @@ "repo": "default", "type": "github" } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "prisma-utils", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1738070913, + "narHash": "sha256-j6jC12vCFsTGDmY2u1H12lMr62fnclNjuCtAdF1a4Nk=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "bebf27d00f7d10ba75332a0541ac43676985dea3", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 7c1a0a42..79e4ae47 100644 --- a/flake.nix +++ b/flake.nix @@ -1,37 +1,77 @@ { description = "CourseMate"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/master"; + nixpkgs.url = "github:NixOS/nixpkgs/release-24.11"; + + nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + flake-utils.url = "github:numtide/flake-utils"; - prisma-utils.url = "github:VanCoding/nix-prisma-utils"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + prisma-utils = { + url = "github:VanCoding/nix-prisma-utils"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; - outputs = { nixpkgs, flake-utils, prisma-utils, ... }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = import nixpkgs { inherit system; }; + outputs = { + nixpkgs, + flake-utils, + rust-overlay, + prisma-utils, + nixpkgs-unstable, + ... + }: + flake-utils.lib.eachDefaultSystem (system: let + overlays = [(import rust-overlay)]; + pkgs = import nixpkgs { + inherit system overlays; + }; + unstable = nixpkgs-unstable.legacyPackages.${system}; + + rust-bin = pkgs.rust-bin.fromRustupToolchainFile ./scraper/rust-toolchain.toml; + prisma = pkgs.callPackage ./server/prisma.nix {inherit prisma-utils;}; - prisma = (prisma-utils.lib.prisma-factory { - nixpkgs = pkgs; - prisma-fmt-hash = "sha256-atD5GZfmeU86mF1V6flAshxg4fFR2ews7EwaJWZZzbc="; # just copy these hashes for now, and then change them when nix complains about the mismatch - query-engine-hash = "sha256-8FTZaKmQCf9lrDQvkF5yWPeZ7TSVfFjTbjdbWWEHgq4="; - libquery-engine-hash = "sha256-USIdaum87ekGY6F6DaL/tKH0BAZvHBDK7zjmCLo//kM="; - schema-engine-hash = "sha256-k5MkxXViEqojbkkcW/4iBFNdfhb9PlMEF1M2dyhfOok="; - }).fromNpmLock - ./server/package-lock.json; # <--- path to our package-lock.json file that contains the version of prisma-engines - in - { - devShell = pkgs.mkShell { - src = ./.; - nativeBuildInputs = with pkgs; [ bashInteractive ]; - buildInputs = with pkgs; [ + common = { + packages = + (with pkgs; [ + nix # HACK: to fix the side effect of the hack below, installing two instances of nix + flyctl gnumake - bun + nodejs biome + lefthook + dotenv-cli + ]) + ++ (with unstable; [ + bun + ]); + + env = + prisma.env + // { + # HACK: sharp can't find libstdc++.so.6 on bun without this + # - hack because: setting this may break other packages + # - info: it can find libstdc++.so.6 on Node.js + # - info: NobbZ says it's because "We can not set an rpath for a scripting language" + LD_LIBRARY_PATH = "${pkgs.stdenv.cc.cc.lib}/lib"; + }; + }; + in { + packages.scraper = pkgs.callPackage ./scraper {toolchain = rust-bin;}; + devShells.default = pkgs.mkShell common; + devShells.scraper = pkgs.mkShell { + inherit (common) env; + packages = + common.packages + ++ [ + pkgs.pkg-config + pkgs.openssl + rust-bin ]; - shellHook = '' - export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${pkgs.stdenv.cc.cc.lib}/lib - '' + (if pkgs.system == "x86_64-linux" then prisma.shellHook else ""); - }; - }); + }; + }); } diff --git a/fly.toml b/fly.toml new file mode 100644 index 00000000..0c62208c --- /dev/null +++ b/fly.toml @@ -0,0 +1,24 @@ +# fly.toml app configuration file generated for coursemate on 2025-03-06T14:33:10+09:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'coursemate' +primary_region = 'nrt' + +[build] + +[deploy] + +[http_service] +internal_port = 3000 +force_https = true +auto_stop_machines = 'stop' +auto_start_machines = true +min_machines_running = 0 +processes = ['app'] + +[[vm]] +memory = '512mb' +cpu_kind = 'shared' +cpus = 1 diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 00000000..58f9e35e --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,16 @@ +# Lefthook: Precommit Hook +# docs: https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md +# 必須: Lefthook +# 必須: `lefthook install` +pre-commit: + parallel: true + commands: + biome: + glob: "*" + run: bunx biome check --write --no-errors-on-unmatched --files-ignore-unknown=true -- {staged_files} + stage_fixed: true + prevent-commit-on-main: + run: if [ "$(git branch --show-current)" == "main" ]; then echo "Cannot make commit on main! aborting..."; exit 1; fi + cspell: + glob: "*" + run: bun run spell diff --git a/package.json b/package.json index 8b60397f..04956ad3 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,42 @@ { "name": "course-mate", + "private": true, "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "prepare": "husky" - }, - "keywords": [], "author": "", - "license": "ISC", + "main": "index.js", "devDependencies": { "@biomejs/biome": "^1.9.1", - "husky": "^9.1.4", - "lint-staged": "^15.2.10", - "typescript": "^5.6.2" + "lefthook": "^1.10.10", + "lint-staged": "^15.2.10" }, - "dependencies": { - "@types/bun": "^1.1.10", - "cspell": "^8.14.4", - "zod": "^3.23.8" + "description": "", + "keywords": [], + "license": "ISC", + "scripts": { + "prepare": "lefthook install && (cd server; bun run prepare)", + "check": "bun type && bun style:check", + "style": "biome check . --fix", + "style:check": "biome check .", + "type": "bun type:web && bun type:server && bun type:common", + "type:web": "cd web; bun run tsc", + "type:server": "cd server; bun run tsc", + "type:common": "cd common; bun run tsc", + "build": "bun run build:server && bun run build:web", + "build:web": "cd web; bun run build", + "build:server": "cd server; bun run build", + "watch": "trap 'kill 0' EXIT; bun run watch:web & bun run watch:server & wait", + "watch:web": "cd web; bun run dev", + "watch:server": "cd server; bun run dev", + "seed": "cd server; bun prisma db seed", + "prepare:deploy:web": "bun install && bun run build:web", + "deploy:web": "cd web; bun run start --port $PORT", + "deploy:server": "bun run --env-file=.env :deploy:server", + ":deploy:server": "fly deploy --build-arg SQL_GENERATE_URL=$SQL_GENERATE_URL", + "docker:server": "docker build . --build-arg SQL_GENERATE_URL=$SQL_GENERATE_URL", + "dev-db": "make dev-db", + "test": "make test", + "spell": "bunx cspell --quiet ." }, - "lint-staged": { - "*.{js,jsx,ts,tsx}": ["biome check --write"] - } + "trustedDependencies": ["@biomejs/biome", "lefthook"], + "workspaces": ["common", "web", "server"] } diff --git a/render.yaml b/render.yaml new file mode 100644 index 00000000..750f305e --- /dev/null +++ b/render.yaml @@ -0,0 +1,33 @@ +# Exported from Render on 2025-02-15T11:38:48Z +services: +- type: web + name: CourseMate + runtime: static + repo: https://github.com/ut-code/CourseMate + branch: make-it-ssg + envVars: + - key: BUN_VERSION + sync: false # Render Dashboard https://render.com/docs/blueprint-spec#prompting-for-secret-values + - key: SKIP_INSTALL_DEPS + sync: false + - key: NEXT_PUBLIC_ALLOW_ANY_MAIL_ADDR + sync: false + - key: NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID + sync: false + - key: NEXT_PUBLIC_FIREBASE_APP_ID + sync: false + - key: NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID + sync: false + - key: NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET + sync: false + - key: NEXT_PUBLIC_FIREBASE_PROJECT_ID + sync: false + - key: NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN + sync: false + - key: NEXT_PUBLIC_FIREBASE_API_KEY + sync: false + - key: NEXT_PUBLIC_API_ENDPOINT + sync: false + buildCommand: bun install --filter "web" && cd web && bun run build + staticPublishPath: web/out +version: "1" diff --git a/scraper/.gitignore b/scraper/.gitignore new file mode 100644 index 00000000..c5903a8d --- /dev/null +++ b/scraper/.gitignore @@ -0,0 +1,5 @@ +/target +/data.json +.cache +/course-mate-scraper + diff --git a/scraper/Cargo.lock b/scraper/Cargo.lock new file mode 100644 index 00000000..15fe7b20 --- /dev/null +++ b/scraper/Cargo.lock @@ -0,0 +1,1907 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "cc" +version = "1.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "coursemate-scraper" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "futures", + "lazy_static", + "reqwest", + "scraper", + "serde", + "serde_json", + "sha2", + "tokio", +] + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.11.2", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "ego-tree" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12a0bb14ac04a9fcf170d0bbbef949b44cc492f4452bd20c095636956f653642" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf 0.11.2", + "phf_codegen 0.11.2", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro2" +version = "1.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "reqwest" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scraper" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90460b31bfe1fc07be8262e42c665ad97118d4585869de9345a84d501a9eaf0" +dependencies = [ + "ahash", + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "once_cell", + "selectors", + "tendril", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" +dependencies = [ + "bitflags", + "cssparser", + "derive_more", + "fxhash", + "log", + "new_debug_unreachable", + "phf 0.10.1", + "phf_codegen 0.10.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "servo_arc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/scraper/Cargo.toml b/scraper/Cargo.toml new file mode 100644 index 00000000..b30cb47c --- /dev/null +++ b/scraper/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "coursemate-scraper" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.89" +chrono = "0.4.38" +futures = "0.3.31" +lazy_static = "1.5.0" +reqwest = "0.12.8" +scraper = "0.20.0" +serde = { version = "1.0.210", features = ["serde_derive"] } +serde_json = "1.0.128" +sha2 = "0.10.8" +tokio = { version = "1.40.0", features = ["full"] } diff --git a/scraper/default.nix b/scraper/default.nix new file mode 100644 index 00000000..cbb6edf0 --- /dev/null +++ b/scraper/default.nix @@ -0,0 +1,21 @@ +{ + lib, + stdenv, + openssl, + pkg-config, + makeRustPlatform, + toolchain, +}: let + rustPlatform = makeRustPlatform { + cargo = toolchain; + rustc = toolchain; + }; +in + rustPlatform.buildRustPackage { + buildInputs = lib.lists.optional stdenv.isLinux openssl; + nativeBuildInputs = lib.lists.optional stdenv.isLinux pkg-config; + pname = "coursemate-scraper"; + version = "0.1.0"; + src = ./.; + cargoLock.lockFile = ./Cargo.lock; + } diff --git a/scraper/readme.md b/scraper/readme.md new file mode 100644 index 00000000..9b1b87c2 --- /dev/null +++ b/scraper/readme.md @@ -0,0 +1,25 @@ +# 後期課程の授業をスクレイピングするスクリプト + +https://catalog.he.u-tokyo.ac.jp/result にある授業情報を取得するスクリプトです。 + +## Quick Start + +以下のコマンドを実行すると、 `data.json` に授業情報がjson形式で保存されます。 + +```bash +cd /path/to/this/dir +cargo run --release +``` + +## Maintaining + +### Add faculty + +ページに移動したときに、左側に学科を選ぶとhttps://catalog.he.u-tokyo.ac.jp/result?type=ug&faculty_id=1のようになります。 +faculty_idが学科のIDです。 +urls.rsのUrlsに学部の名前とその url の tuple を追加してください。 +すでに全ての学科のIDが入っているので、特に追加するような状況にならない限りは変更する必要はありません。 + +### Extending code + +コード中に .unwrap() が多くあると思います。これは意図的です。 diff --git a/scraper/rust-toolchain.toml b/scraper/rust-toolchain.toml new file mode 100644 index 00000000..215f1208 --- /dev/null +++ b/scraper/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "stable" +components = ["rustc", "cargo", "rustfmt", "clippy"] +targets = [] diff --git a/scraper/src/io.rs b/scraper/src/io.rs new file mode 100644 index 00000000..b6b3cba2 --- /dev/null +++ b/scraper/src/io.rs @@ -0,0 +1,36 @@ +use crate::types::*; +use anyhow::ensure; +use sha2::{Digest, Sha256}; +use tokio::fs; +use tokio::io::AsyncWriteExt; + +pub async fn write_to(file: &mut fs::File, content: Entry) -> anyhow::Result<()> { + let s = serde_json::to_string(&content)?; + file.write_all(s.as_bytes()).await?; + Ok(()) +} + +use crate::CACHE_DIR; + +pub async fn request(url: &str) -> anyhow::Result { + println!("[request] sending request to {}", url); + + let hash = Sha256::digest(url.as_bytes()); + let path = format!("{CACHE_DIR}/{:x}", hash); + if let Ok(bytes) = fs::read(&path).await { + if let Ok(text) = String::from_utf8(bytes) { + return Ok(text); + } + } + let res = reqwest::get(url).await?; + let status = res.status().as_u16(); + ensure!( + (200..=299).contains(&status), + "Status check failed: expected 200~299, got {}", + status + ); + let text = res.text().await?; + let mut f = tokio::fs::File::create(path).await?; + f.write_all(text.as_bytes()).await?; + Ok(text) +} diff --git a/scraper/src/logger.rs b/scraper/src/logger.rs new file mode 100644 index 00000000..c2311d87 --- /dev/null +++ b/scraper/src/logger.rs @@ -0,0 +1,43 @@ +pub struct Logger { + limit: usize, + current: usize, + start_ms: i64, + start_sec: i64, + start_min: i64, +} + +impl Logger { + pub fn new(limit: usize) -> Self { + let start_ms = chrono::Local::now().timestamp_millis(); + let start_sec = chrono::Local::now().timestamp(); + let start_min = chrono::Local::now().timestamp() / 60; + Logger { + current: 0, + limit, + start_ms, + start_sec, + start_min, + } + } + pub fn done(&mut self, name: &str) { + let now_ms = chrono::Local::now().timestamp_millis(); + let now_sec = chrono::Local::now().timestamp(); + let now_min = now_sec / 60; + self.current += 1; + let count = self.current; + println!( + "[log] faculty {name} done. ({count} / {}) timestamp: {}ms / {}sec / {}min", + self.limit, + now_ms - self.start_ms, + now_sec - self.start_sec, + now_min - self.start_min, + ); + } + pub fn close(self) -> Result<(), ()> { + if self.limit == self.current { + Ok(()) + } else { + Err(()) + } + } +} diff --git a/scraper/src/main.rs b/scraper/src/main.rs new file mode 100644 index 00000000..d3fd67b6 --- /dev/null +++ b/scraper/src/main.rs @@ -0,0 +1,99 @@ +mod io; +mod logger; +mod parser; +mod types; +mod urls; + +use lazy_static::lazy_static; +use std::time::Duration; +use tokio::{fs, io::AsyncWriteExt}; + +use anyhow::Context; +use tokio::time::sleep; +use types::*; + +use scraper::{Html, Selector}; +use urls::URLS; + +const RESULT_FILE: &str = "./data.json"; +const CACHE_DIR: &str = "./.cache"; + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + println!("[log] starting..."); + + let _ = fs::DirBuilder::new().create(CACHE_DIR).await; + + let mut file = fs::File::create(RESULT_FILE) + .await + .expect("Failed to create file"); + file.write_all("[".as_bytes()).await.unwrap(); + + let total = URLS.len(); + let mut logger = logger::Logger::new(total); + for (faculty_name, base_url) in URLS { + let courses = get_courses_of(base_url).await; + logger.done(faculty_name); + let result = Entry { + name: faculty_name.to_owned(), + courses, + }; + io::write_to(&mut file, result).await.unwrap(); + file.write_all(",".as_bytes()).await.unwrap(); + } + + file.write_all("]".as_bytes()).await.unwrap(); + logger.close().unwrap(); +} + +async fn get_courses_of(base_url: &str) -> Vec { + let courses = page_index_pages(base_url) + .await + .into_iter() + .map(|content_page_url| async { + let html = scrape(&content_page_url).await; + parser::parse_course_info(html) + .context(content_page_url) + .unwrap() + }); + futures::future::join_all(courses) + .await + .into_iter() + .collect::>() +} + +lazy_static! { + static ref DETAIL_BUTTONS: Selector = + Selector::parse(".catalog-search-result-card-header-detail-link") + .expect("invalid selector"); +} +const BASE_URL: &str = "https://catalog.he.u-tokyo.ac.jp/"; + +async fn page_index_pages(base_url: &str) -> Vec { + let mut urls: Vec = Vec::new(); + for key in 0.. { + let html = scrape(&format!("{}{}", base_url, key)).await; + if html.select(&DETAIL_BUTTONS).next().is_none() { + break; + } + urls.extend( + html.select(&DETAIL_BUTTONS) + .map(|elem| BASE_URL.to_owned() + elem.attr("href").unwrap()), + ); + } + urls +} + +async fn scrape(url: &str) -> Html { + for tries in 0..10 { + let res = io::request(url).await; + match res { + Ok(val) => return Html::parse_document(&val), + Err(err) => { + eprintln!("request error: {err} for {} times", tries + 1); + sleep(Duration::from_millis(200)).await; + } + } + } + panic!("Request failed too many times"); +} diff --git a/scraper/src/parser.rs b/scraper/src/parser.rs new file mode 100644 index 00000000..f3214ed2 --- /dev/null +++ b/scraper/src/parser.rs @@ -0,0 +1,57 @@ +use anyhow::anyhow; +use lazy_static::lazy_static; +use scraper::{Html, Selector}; + +use crate::types::*; + +lazy_static! { + static ref NAME_SELECTOR: Selector = + Selector::parse(".catalog-page-detail-table-cell.name-cell").unwrap(); + static ref TEACHER_SELECTOR: Selector = + Selector::parse(".catalog-page-detail-table-cell.lecturer-cell").unwrap(); + static ref SEMESTER_SELECTOR: Selector = + Selector::parse(".catalog-page-detail-table-cell.semester-cell").unwrap(); + static ref PERIOD_SELECTOR: Selector = + Selector::parse(".catalog-page-detail-table-cell.period-cell").unwrap(); + static ref CODE_SELECTOR: Selector = + Selector::parse(".catalog-page-detail-table-cell.code-cell").unwrap(); +} + +pub fn parse_course_info(html: Html) -> anyhow::Result { + Ok(Course { + name: select(&html, &NAME_SELECTOR, 1)?, + teacher: select(&html, &TEACHER_SELECTOR, 1)?, + semester: select_all(&html, &SEMESTER_SELECTOR, 1)?.join(","), + period: select(&html, &PERIOD_SELECTOR, 1)?, + code: select_all(&html, &CODE_SELECTOR, 1)?.join(" "), + }) +} + +fn select(html: &Html, selector: &Selector, nth: usize) -> anyhow::Result { + html.select(selector) + .nth(nth) + .ok_or(anyhow!( + "Couldn't find matching element for selector {:?}", + selector, + )) + .map(|val| val.text().next().unwrap().trim().to_owned()) +} + +fn select_all<'a>( + html: &'a Html, + selector: &'static Selector, + nth: usize, +) -> anyhow::Result> { + html.select(selector) + .nth(nth) + .ok_or(anyhow!( + "Couldn't find matching element for selector {:?}", + selector, + )) + .map(|val| { + val.text() + .map(|text| text.trim()) + .filter(|&text| !text.is_empty()) + .collect::>() + }) +} diff --git a/scraper/src/types.rs b/scraper/src/types.rs new file mode 100644 index 00000000..664d3c41 --- /dev/null +++ b/scraper/src/types.rs @@ -0,0 +1,15 @@ +use serde::Serialize; + +#[derive(Serialize, Clone)] +pub struct Course { + pub name: String, + pub teacher: String, + pub semester: String, + pub period: String, + pub code: String, +} +#[derive(Serialize, Clone)] +pub struct Entry { + pub name: String, + pub courses: Vec, +} diff --git a/scraper/src/urls.rs b/scraper/src/urls.rs new file mode 100644 index 00000000..aaa7f78c --- /dev/null +++ b/scraper/src/urls.rs @@ -0,0 +1,42 @@ +pub static URLS: [(&str, &str); 10] = [ + ( + "law", + "https://catalog.he.u-tokyo.ac.jp/result?type=ug&faculty_id=1&page=", + ), + ( + "medicine", + "https://catalog.he.u-tokyo.ac.jp/result?type=ug&faculty_id=2&page=", + ), + ( + "engineering", + "https://catalog.he.u-tokyo.ac.jp/result?type=ug&faculty_id=3&page=", + ), + ( + "arts", + "https://catalog.he.u-tokyo.ac.jp/result?type=ug&faculty_id=4&page=", + ), + ( + "science", + "https://catalog.he.u-tokyo.ac.jp/result?type=ug&faculty_id=5&page=", + ), + ( + "agriculture", + "https://catalog.he.u-tokyo.ac.jp/result?type=ug&faculty_id=6&page=", + ), + ( + "economics", + "https://catalog.he.u-tokyo.ac.jp/result?type=ug&faculty_id=7&page=", + ), + ( + "liberal_arts", + "https://catalog.he.u-tokyo.ac.jp/result?type=ug&faculty_id=8&page=", + ), + ( + "education", + "https://catalog.he.u-tokyo.ac.jp/result?type=ug&faculty_id=9&page=", + ), + ( + "pharmacy", + "https://catalog.he.u-tokyo.ac.jp/result?type=ug&faculty_id=10&page=", + ), +]; diff --git a/server/.env.sample b/server/.env.sample index bb0e5bca..b5343ada 100644 --- a/server/.env.sample +++ b/server/.env.sample @@ -1,12 +1,14 @@ # Database # DATABASE_URL=postgres://johndoe:supersecretpassword@example.com:5432/dbname # below can be used for docker db created via `make dev-db` -DATABASE_URL=postgres://user:password@localhost:5432/database +DATABASE_URL=postgres://user:password@localhost:6543/database +DIRECT_URL=postgres://user:password@localhost:5432/database -# Application origins -SERVER_ORIGIN=http://localhost:3000 -WEB_ORIGIN=http://localhost:5173 -MOBILE_ORIGIN=http://localhost:8081 +# CORS allow origins, separated by "," | no space is allowed before/after "," +CORS_ALLOW_ORIGINS=http://localhost:3000,http://localhost:5173 # Firebase FIREBASE_PROJECT_ID=project-id + +# Enable SQL Log (server/src/database/client.ts) +SQL_LOG=true diff --git a/server/.gitignore b/server/.gitignore index c0bced27..f7361e03 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -2,8 +2,6 @@ node_modules # WARNING: DELETE THIS IF YOU FIND THIS .env - .env.dev -# deprecated, delete /dist if you have one -/dist + /target diff --git a/server/Dockerfile b/server/Dockerfile deleted file mode 100644 index 99b960c8..00000000 --- a/server/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM oven/bun:latest -WORKDIR /coursemate/dev/server - -COPY package.json package-lock.json ./ -RUN npm ci - -COPY ./prisma ./ -RUN npx prisma generate - -COPY . . - -RUN npm run build -CMD npx prisma db push && npm run serve diff --git a/server/bun.lockb b/server/bun.lockb deleted file mode 100755 index 936692b0..00000000 Binary files a/server/bun.lockb and /dev/null differ diff --git a/server/package-lock.json b/server/package-lock.json deleted file mode 100644 index 704db8b5..00000000 --- a/server/package-lock.json +++ /dev/null @@ -1,3656 +0,0 @@ -{ - "name": "course-mate", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "course-mate", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@prisma/client": "^5.20.0", - "@types/cors": "^2.8.17", - "cookie-parser": "^1.4.6", - "cors": "^2.8.5", - "dotenv": "^16.4.5", - "express": "^4.21.0", - "firebase-admin": "^12.5.0", - "sharp": "^0.33.5", - "socket.io": "^4.8.0", - "zod": "^3.23.8" - }, - "devDependencies": { - "@types/cookie-parser": "^1.4.7", - "@types/express": "^4.17.21", - "@types/react": "^18.3.10", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "globals": "^15.9.0", - "prisma": "^5.20.0", - "typescript": "^5.6.2", - "vite": "^5.4.8" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/code-frame/node_modules/picocolors": { - "version": "1.0.1", - "dev": true, - "license": "ISC" - }, - "node_modules/@babel/compat-data": { - "version": "7.24.8", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.24.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.8", - "@babel/helper-compilation-targets": "^7.24.8", - "@babel/helper-module-transforms": "^7.24.8", - "@babel/helpers": "^7.24.8", - "@babel/parser": "^7.24.8", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.8", - "@babel/types": "^7.24.8", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/debug": { - "version": "4.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/core/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.24.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.8", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.24.8", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.24.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.24.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.8" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/picocolors": { - "version": "1.0.1", - "dev": true, - "license": "ISC" - }, - "node_modules/@babel/parser": { - "version": "7.24.8", - "dev": true, - "license": "MIT", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.24.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.8", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.8", - "@babel/types": "^7.24.8", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/debug": { - "version": "4.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/traverse/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/types": { - "version": "7.24.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@fastify/busboy": { - "version": "3.0.0", - "license": "MIT" - }, - "node_modules/@firebase/app-check-interop-types": { - "version": "0.3.2", - "license": "Apache-2.0" - }, - "node_modules/@firebase/app-types": { - "version": "0.9.2", - "license": "Apache-2.0" - }, - "node_modules/@firebase/auth-interop-types": { - "version": "0.2.3", - "license": "Apache-2.0" - }, - "node_modules/@firebase/database-types": { - "version": "1.0.4", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-types": "0.9.2", - "@firebase/util": "1.9.7" - } - }, - "node_modules/@firebase/database-types/node_modules/@firebase/util": { - "version": "1.9.7", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@firebase/logger": { - "version": "0.4.2", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@google-cloud/firestore": { - "version": "7.9.0", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "functional-red-black-tree": "^1.0.1", - "google-gax": "^4.3.3", - "protobufjs": "^7.2.6" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/paginator": { - "version": "5.0.2", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/projectify": { - "version": "4.0.0", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/promisify": { - "version": "4.0.0", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/storage": { - "version": "7.12.0", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@google-cloud/paginator": "^5.0.0", - "@google-cloud/projectify": "^4.0.0", - "@google-cloud/promisify": "^4.0.0", - "abort-controller": "^3.0.0", - "async-retry": "^1.3.3", - "duplexify": "^4.1.3", - "fast-xml-parser": "^4.3.0", - "gaxios": "^6.0.2", - "google-auth-library": "^9.6.3", - "html-entities": "^2.5.2", - "mime": "^3.0.0", - "p-limit": "^3.0.1", - "retry-request": "^7.0.0", - "teeny-request": "^9.0.0", - "uuid": "^8.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/storage/node_modules/mime": { - "version": "3.0.0", - "license": "MIT", - "optional": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@google-cloud/storage/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "optional": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.13", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.2.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "license": "MIT", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "node_modules/@prisma/client": { - "version": "5.20.0", - "hasInstallScript": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.13" - }, - "peerDependencies": { - "prisma": "*" - }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - } - } - }, - "node_modules/@prisma/debug": { - "version": "5.20.0", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/engines": { - "version": "5.20.0", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "5.20.0", - "@prisma/engines-version": "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284", - "@prisma/fetch-engine": "5.20.0", - "@prisma/get-platform": "5.20.0" - } - }, - "node_modules/@prisma/engines-version": { - "version": "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/fetch-engine": { - "version": "5.20.0", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "5.20.0", - "@prisma/engines-version": "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284", - "@prisma/get-platform": "5.20.0" - } - }, - "node_modules/@prisma/get-platform": { - "version": "5.20.0", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "5.20.0" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.22.4", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.22.4", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "license": "MIT" - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/caseless": { - "version": "0.12.5", - "license": "MIT", - "optional": true - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cookie": { - "version": "0.4.1", - "license": "MIT" - }, - "node_modules/@types/cookie-parser": { - "version": "1.4.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/cors": { - "version": "2.8.17", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/express": { - "version": "4.17.21", - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.5", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "license": "MIT" - }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.6", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/long": { - "version": "4.0.2", - "license": "MIT", - "optional": true - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.14.10", - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.12", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/qs": { - "version": "6.9.15", - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.10", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/react-dom/node_modules/@types/react": { - "version": "18.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/request": { - "version": "2.48.12", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.0" - } - }, - "node_modules/@types/send": { - "version": "0.17.4", - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "license": "MIT", - "optional": true - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-react-jsx-self": "^7.24.5", - "@babel/plugin-transform-react-jsx-source": "^7.24.1", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "license": "MIT", - "optional": true, - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/agent-base": { - "version": "7.1.1", - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/agent-base/node_modules/debug": { - "version": "4.3.5", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/agent-base/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "license": "MIT", - "optional": true - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ansi-styles/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/ansi-styles/node_modules/color-convert/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "license": "MIT" - }, - "node_modules/arrify": { - "version": "2.0.1", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async-retry": { - "version": "1.3.3", - "license": "MIT", - "optional": true, - "dependencies": { - "retry": "0.13.1" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "license": "MIT", - "optional": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/base64id": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, - "node_modules/bignumber.js": { - "version": "9.1.2", - "license": "MIT", - "optional": true, - "engines": { - "node": "*" - } - }, - "node_modules/body-parser": { - "version": "1.20.3", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/browserslist": { - "version": "4.23.2", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001640", - "electron-to-chromium": "^1.4.820", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.1.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "license": "BSD-3-Clause" - }, - "node_modules/bytes": { - "version": "3.1.2", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001641", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "license": "ISC", - "optional": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/color": { - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "license": "MIT", - "optional": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.4.1", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-parser": { - "version": "1.4.6", - "license": "MIT", - "dependencies": { - "cookie": "0.4.1", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "2.6.9", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.0.3", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "16.4.5", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/duplexify": { - "version": "4.1.3", - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.4.827", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "license": "MIT", - "optional": true - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "license": "MIT", - "optional": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/engine.io": { - "version": "6.6.1", - "license": "MIT", - "dependencies": { - "@types/cookie": "^0.4.1", - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.4.1", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io/node_modules/@types/node": { - "version": "22.5.5", - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/engine.io/node_modules/@types/node/node_modules/undici-types": { - "version": "6.19.8", - "license": "MIT" - }, - "node_modules/engine.io/node_modules/cookie": { - "version": "0.4.2", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.3.5", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.21.5", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escalade": { - "version": "3.1.2", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "4.21.0", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.6.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "license": "MIT", - "optional": true - }, - "node_modules/farmhash-modern": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "license": "MIT", - "optional": true - }, - "node_modules/fast-xml-parser": { - "version": "4.4.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/firebase-admin": { - "version": "12.5.0", - "license": "Apache-2.0", - "dependencies": { - "@fastify/busboy": "^3.0.0", - "@firebase/database-compat": "^1.0.2", - "@firebase/database-types": "^1.0.0", - "@types/node": "^22.0.1", - "farmhash-modern": "^1.1.0", - "jsonwebtoken": "^9.0.0", - "jwks-rsa": "^3.1.0", - "node-forge": "^1.3.1", - "uuid": "^10.0.0" - }, - "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "@google-cloud/firestore": "^7.7.0", - "@google-cloud/storage": "^7.7.0" - } - }, - "node_modules/firebase-admin/node_modules/@firebase/database-compat": { - "version": "1.0.6", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.6.8", - "@firebase/database": "1.0.6", - "@firebase/database-types": "1.0.4", - "@firebase/logger": "0.4.2", - "@firebase/util": "1.9.7", - "tslib": "^2.1.0" - } - }, - "node_modules/firebase-admin/node_modules/@firebase/database-compat/node_modules/@firebase/component": { - "version": "0.6.8", - "license": "Apache-2.0", - "dependencies": { - "@firebase/util": "1.9.7", - "tslib": "^2.1.0" - } - }, - "node_modules/firebase-admin/node_modules/@firebase/database-compat/node_modules/@firebase/database": { - "version": "1.0.6", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check-interop-types": "0.3.2", - "@firebase/auth-interop-types": "0.2.3", - "@firebase/component": "0.6.8", - "@firebase/logger": "0.4.2", - "@firebase/util": "1.9.7", - "faye-websocket": "0.11.4", - "tslib": "^2.1.0" - } - }, - "node_modules/firebase-admin/node_modules/@firebase/database-compat/node_modules/@firebase/util": { - "version": "1.9.7", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/firebase-admin/node_modules/@types/node": { - "version": "22.5.5", - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/firebase-admin/node_modules/@types/node/node_modules/undici-types": { - "version": "6.19.8", - "license": "MIT" - }, - "node_modules/form-data": { - "version": "2.5.1", - "license": "MIT", - "optional": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "license": "MIT", - "optional": true - }, - "node_modules/gaxios": { - "version": "6.7.0", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^10.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/gcp-metadata": { - "version": "6.1.0", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "gaxios": "^6.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "license": "ISC", - "optional": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globals": { - "version": "15.9.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/google-auth-library": { - "version": "9.11.0", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/google-auth-library/node_modules/jws": { - "version": "4.0.0", - "license": "MIT", - "optional": true, - "dependencies": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/google-auth-library/node_modules/jws/node_modules/jwa": { - "version": "2.0.0", - "license": "MIT", - "optional": true, - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/google-gax": { - "version": "4.3.8", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@grpc/grpc-js": "^1.10.9", - "@grpc/proto-loader": "^0.7.13", - "@types/long": "^4.0.0", - "abort-controller": "^3.0.0", - "duplexify": "^4.0.0", - "google-auth-library": "^9.3.0", - "node-fetch": "^2.6.1", - "object-hash": "^3.0.0", - "proto3-json-serializer": "^2.0.2", - "protobufjs": "^7.3.2", - "retry-request": "^7.0.0", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/google-gax/node_modules/@grpc/grpc-js": { - "version": "1.11.1", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@grpc/proto-loader": "^0.7.13", - "@js-sdsl/ordered-map": "^4.4.2" - }, - "engines": { - "node": ">=12.10.0" - } - }, - "node_modules/google-gax/node_modules/uuid": { - "version": "9.0.1", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "optional": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gtoken": { - "version": "7.1.0", - "license": "MIT", - "optional": true, - "dependencies": { - "gaxios": "^6.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/gtoken/node_modules/jws": { - "version": "4.0.0", - "license": "MIT", - "optional": true, - "dependencies": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/gtoken/node_modules/jws/node_modules/jwa": { - "version": "2.0.0", - "license": "MIT", - "optional": true, - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-entities": { - "version": "2.5.2", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/http-errors": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.8", - "license": "MIT" - }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "license": "MIT", - "optional": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "6.0.2", - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.3.5", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/http-proxy-agent/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "license": "MIT", - "optional": true - }, - "node_modules/https-proxy-agent": { - "version": "7.0.5", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.3.5", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "license": "MIT", - "optional": true - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "license": "MIT" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jose": { - "version": "4.15.9", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "2.5.2", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-bigint": { - "version": "1.0.0", - "license": "MIT", - "optional": true, - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.2", - "license": "MIT", - "dependencies": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jsonwebtoken/node_modules/ms": { - "version": "2.1.2", - "license": "MIT" - }, - "node_modules/jsonwebtoken/node_modules/semver": { - "version": "7.6.2", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jwa": { - "version": "1.4.1", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jwks-rsa": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "@types/express": "^4.17.17", - "@types/jsonwebtoken": "^9.0.2", - "debug": "^4.3.4", - "jose": "^4.14.6", - "limiter": "^1.1.5", - "lru-memoizer": "^2.2.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/jwks-rsa/node_modules/debug": { - "version": "4.3.5", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/jwks-rsa/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "license": "MIT" - }, - "node_modules/jws": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/limiter": { - "version": "1.1.5" - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "license": "MIT", - "optional": true - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "license": "MIT" - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "license": "MIT" - }, - "node_modules/long": { - "version": "5.2.3", - "license": "Apache-2.0", - "optional": true - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lru-memoizer": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "lodash.clonedeep": "^4.5.0", - "lru-cache": "6.0.0" - } - }, - "node_modules/lru-memoizer/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/lru-memoizer/node_modules/lru-cache/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.7", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "license": "MIT", - "optional": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-forge": { - "version": "1.3.1", - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-releases": { - "version": "2.0.14", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.2", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "license": "ISC", - "optional": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "license": "MIT", - "optional": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.10", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.0", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.4.47", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prisma": { - "version": "5.20.0", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/engines": "5.20.0" - }, - "bin": { - "prisma": "build/index.js" - }, - "engines": { - "node": ">=16.13" - }, - "optionalDependencies": { - "fsevents": "2.3.3" - } - }, - "node_modules/proto3-json-serializer": { - "version": "2.0.2", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "protobufjs": "^7.2.5" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/protobufjs": { - "version": "7.3.2", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/react-refresh": { - "version": "0.14.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/retry-request": { - "version": "7.0.2", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/request": "^2.48.8", - "extend": "^3.0.2", - "teeny-request": "^9.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/rollup": { - "version": "4.22.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.22.4", - "@rollup/rollup-android-arm64": "4.22.4", - "@rollup/rollup-darwin-arm64": "4.22.4", - "@rollup/rollup-darwin-x64": "4.22.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", - "@rollup/rollup-linux-arm-musleabihf": "4.22.4", - "@rollup/rollup-linux-arm64-gnu": "4.22.4", - "@rollup/rollup-linux-arm64-musl": "4.22.4", - "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", - "@rollup/rollup-linux-riscv64-gnu": "4.22.4", - "@rollup/rollup-linux-s390x-gnu": "4.22.4", - "@rollup/rollup-linux-x64-gnu": "4.22.4", - "@rollup/rollup-linux-x64-musl": "4.22.4", - "@rollup/rollup-win32-arm64-msvc": "4.22.4", - "@rollup/rollup-win32-ia32-msvc": "4.22.4", - "@rollup/rollup-win32-x64-msvc": "4.22.4", - "fsevents": "~2.3.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.6.3", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.0", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.2", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "license": "ISC" - }, - "node_modules/sharp": { - "version": "0.33.5", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" - } - }, - "node_modules/side-channel": { - "version": "1.0.6", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/socket.io": { - "version": "4.8.0", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.6.0", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.5", - "license": "MIT", - "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" - } - }, - "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.5", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-adapter/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "license": "MIT" - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.5", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "license": "MIT" - }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.3.5", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "license": "MIT" - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stream-events": { - "version": "1.0.5", - "license": "MIT", - "optional": true, - "dependencies": { - "stubs": "^3.0.0" - } - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "license": "MIT", - "optional": true - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "license": "MIT", - "optional": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "license": "MIT", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strnum": { - "version": "1.0.5", - "license": "MIT", - "optional": true - }, - "node_modules/stubs": { - "version": "3.0.0", - "license": "MIT", - "optional": true - }, - "node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/teeny-request": { - "version": "9.0.0", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.9", - "stream-events": "^1.0.5", - "uuid": "^9.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/teeny-request/node_modules/https-proxy-agent": { - "version": "5.0.1", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/teeny-request/node_modules/https-proxy-agent/node_modules/agent-base": { - "version": "6.0.2", - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/teeny-request/node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.3.5", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/teeny-request/node_modules/https-proxy-agent/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "license": "MIT", - "optional": true - }, - "node_modules/teeny-request/node_modules/uuid": { - "version": "9.0.1", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "optional": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "license": "MIT", - "optional": true - }, - "node_modules/tslib": { - "version": "2.6.3", - "license": "0BSD" - }, - "node_modules/type-is": { - "version": "1.6.18", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.6.2", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.0", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/update-browserslist-db/node_modules/picocolors": { - "version": "1.0.1", - "dev": true, - "license": "ISC" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "license": "MIT", - "optional": true - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "10.0.0", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vite": { - "version": "5.4.8", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "license": "BSD-2-Clause", - "optional": true - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "license": "Apache-2.0", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "license": "MIT", - "optional": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles/node_modules/color-convert/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT", - "optional": true - }, - "node_modules/wrappy": { - "version": "1.0.2", - "license": "ISC", - "optional": true - }, - "node_modules/ws": { - "version": "8.17.1", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "license": "MIT", - "optional": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.23.8", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/server/package.json b/server/package.json index a1974ba3..98ee9c07 100644 --- a/server/package.json +++ b/server/package.json @@ -1,39 +1,44 @@ { - "name": "course-mate", + "name": "server", + "private": true, "version": "1.0.0", "description": "", - "main": "index.js", + "main": "src/index.ts", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "dev": "bun --watch src/main.ts", - "build": "tsc", - "serve": "bun target/main.js", - "prisma-generate-sql": "bunx dotenv -e .env.dev -- prisma generate --sql" + "prepare": "bun --env-file=./.env.dev prisma generate --sql || (echo 'please set DATABASE_URL in server/.env.dev' && exit 1)", + "dev": "bun --watch src/index.ts", + "check": "tsc", + "clean": "rm target -r || true", + "build": "bun clean && bun check && bun run :build", + ":build": "bun build ./src/index.ts --target bun --outfile target/index.js --minify" }, "prisma": { - "seed": "bun src/seeds/seed.ts" + "seed": "bun src/seeds/seed-test.ts" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { - "@prisma/client": "^5.20.0", + "@hono/zod-validator": "^0.4.3", + "@prisma/client": "5.22.0", + "common": "workspace:common", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dotenv": "^16.4.5", "dotenv-cli": "^7.4.2", - "express": "^4.18.2", "firebase-admin": "^12.2.0", + "hono": "^4.7.1", + "lru-cache": "^11.0.2", "sharp": "^0.33.5", - "socket.io": "^4.7.5", + "sql-formatter": "^15.4.10", "zod": "^3.23.8" }, "devDependencies": { "@types/cookie-parser": "^1.4.7", "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", "globals": "^15.8.0", - "prisma": "^5.11.0", + "prisma": "5.22.0", "typescript": "^5.4.5" - } + }, + "trustedPackages": ["prisma"] } diff --git a/server/prisma.nix b/server/prisma.nix new file mode 100644 index 00000000..63cb161f --- /dev/null +++ b/server/prisma.nix @@ -0,0 +1,28 @@ +{ + pkgs, + prisma-utils, +}: let + prisma = + (prisma-utils.lib.prisma-factory + { + inherit pkgs; + prisma-fmt-hash = "sha256-iZuomC/KaLF0fQy6RVHwk2qq4DRaG3xj+sWmtLofiMU="; + query-engine-hash = "sha256-Pl/YpYu326qqpbVfczM5RxB8iWXZlewG9vToqzSPIQo="; + libquery-engine-hash = "sha256-ETwMIJMjMgZmjH6QGD7GVwYYlyx9mo2ydEeunFViCjQ="; + schema-engine-hash = "sha256-rzzzPHOpUM3GJvkhU08lQ7rNspdq3RKxMRRW9YZtvhU="; + }) + .fromBunLock + ../bun.lock; + inherit (prisma) package; +in { + inherit (prisma) shellHook; + + # waiting for https://github.com/VanCoding/nix-prisma-utils/pull/10 + env = { + PRISMA_QUERY_ENGINE_BINARY = "${package}/bin/query-engine"; + PRISMA_QUERY_ENGINE_LIBRARY = "${package}/lib/libquery_engine.node"; + PRISMA_INTROSPECTION_ENGINE_BINARY = "${package}/bin/introspection-engine"; + PRISMA_SCHEMA_ENGINE_BINARY = "${package}/bin/schema-engine"; + PRISMA_FMT_BINARY = "${package}/bin/prisma-fmt"; + }; +} diff --git a/server/prisma/migrations/20240910234439_init/migration.sql b/server/prisma/migrations/0_init/migration.sql similarity index 86% rename from server/prisma/migrations/20240910234439_init/migration.sql rename to server/prisma/migrations/0_init/migration.sql index a3f23aa4..57bdba04 100644 --- a/server/prisma/migrations/20240910234439_init/migration.sql +++ b/server/prisma/migrations/0_init/migration.sql @@ -1,5 +1,5 @@ -- CreateEnum -CREATE TYPE "Day" AS ENUM ('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'); +CREATE TYPE "Day" AS ENUM ('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun', 'other'); -- CreateEnum CREATE TYPE "MatchingStatus" AS ENUM ('PENDING', 'MATCHED', 'REJECTED'); @@ -8,21 +8,30 @@ CREATE TYPE "MatchingStatus" AS ENUM ('PENDING', 'MATCHED', 'REJECTED'); CREATE TABLE "User" ( "id" SERIAL NOT NULL, "guid" TEXT NOT NULL, - "name" TEXT NOT NULL DEFAULT '名無し', - "pictureUrl" TEXT NOT NULL DEFAULT '', - "grade" TEXT NOT NULL DEFAULT '', - "gender" TEXT NOT NULL DEFAULT '', - "hobby" TEXT NOT NULL DEFAULT '', - "intro_short" TEXT NOT NULL DEFAULT '', - "intro_long" TEXT NOT NULL DEFAULT '', + "name" TEXT NOT NULL, + "gender" TEXT NOT NULL, + "grade" TEXT NOT NULL, + "faculty" TEXT NOT NULL, + "department" TEXT NOT NULL, + "intro" TEXT NOT NULL, + "pictureUrl" TEXT NOT NULL, CONSTRAINT "User_pkey" PRIMARY KEY ("id") ); +-- CreateTable +CREATE TABLE "Avatar" ( + "guid" TEXT NOT NULL, + "data" BYTEA NOT NULL, + + CONSTRAINT "Avatar_pkey" PRIMARY KEY ("guid") +); + -- CreateTable CREATE TABLE "Course" ( "id" TEXT NOT NULL, "name" TEXT NOT NULL, + "teacher" TEXT NOT NULL, CONSTRAINT "Course_pkey" PRIMARY KEY ("id") ); @@ -94,20 +103,21 @@ CREATE UNIQUE INDEX "Relationship_sendingUserId_receivingUserId_key" ON "Relatio -- AddForeignKey ALTER TABLE "Slot" ADD CONSTRAINT "Slot_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE RESTRICT ON UPDATE CASCADE; --- AddForeignKey -ALTER TABLE "Enrollment" ADD CONSTRAINT "Enrollment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - -- AddForeignKey ALTER TABLE "Enrollment" ADD CONSTRAINT "Enrollment_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Relationship" ADD CONSTRAINT "Relationship_sendingUserId_fkey" FOREIGN KEY ("sendingUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "Enrollment" ADD CONSTRAINT "Enrollment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Relationship" ADD CONSTRAINT "Relationship_receivingUserId_fkey" FOREIGN KEY ("receivingUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; +-- AddForeignKey +ALTER TABLE "Relationship" ADD CONSTRAINT "Relationship_sendingUserId_fkey" FOREIGN KEY ("sendingUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE "Message" ADD CONSTRAINT "Message_relationId_fkey" FOREIGN KEY ("relationId") REFERENCES "Relationship"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Message" ADD CONSTRAINT "Message_sharedRoomId_fkey" FOREIGN KEY ("sharedRoomId") REFERENCES "SharedRoom"("id") ON DELETE CASCADE ON UPDATE CASCADE; + diff --git a/server/prisma/migrations/1_v2/migration.sql b/server/prisma/migrations/1_v2/migration.sql new file mode 100644 index 00000000..7daf56bd --- /dev/null +++ b/server/prisma/migrations/1_v2/migration.sql @@ -0,0 +1,43 @@ +-- AlterTable +ALTER TABLE "Message" ADD COLUMN "isPicture" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "read" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "pictureUrl" SET DEFAULT '/avatar.svg'; + +-- CreateTable +CREATE TABLE "Picture" ( + "hash" TEXT NOT NULL, + "data" BYTEA NOT NULL, + "key" TEXT NOT NULL, + + CONSTRAINT "Picture_pkey" PRIMARY KEY ("hash") +); + +-- CreateTable +CREATE TABLE "InterestSubject" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "group" TEXT NOT NULL, + + CONSTRAINT "InterestSubject_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Interest" ( + "userId" INTEGER NOT NULL, + "subjectId" INTEGER NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "InterestSubject_name_group_key" ON "InterestSubject"("name", "group"); + +-- CreateIndex +CREATE UNIQUE INDEX "Interest_userId_subjectId_key" ON "Interest"("userId", "subjectId"); + +-- AddForeignKey +ALTER TABLE "Interest" ADD CONSTRAINT "Interest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Interest" ADD CONSTRAINT "Interest_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "InterestSubject"("id") ON DELETE CASCADE ON UPDATE CASCADE; + diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index c4cdaefd..8f6bb750 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -1,13 +1,15 @@ //schema.prisma generator client { - provider = "prisma-client-js" - binaryTargets = ["native", "debian-openssl-3.0.x"] - previewFeatures = ["typedSql"] + provider = "prisma-client-js" + binaryTargets = ["native", "debian-openssl-3.0.x"] + previewFeatures = ["typedSql","relationJoins"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") + // https://github.com/prisma/prisma/discussions/25106#discussioncomment-10500102 + directUrl = env("DIRECT_URL") } enum Day { @@ -33,20 +35,45 @@ model User { faculty String department String intro String - pictureUrl String + pictureUrl String @default("/avatar.svg") // bindings to other parts of this app enrollments Enrollment[] sendingUsers Relationship[] @relation("sending") // 自分がマッチリクエストを送ったユーザー receivingUsers Relationship[] @relation("receiving") // 自分にマッチリクエストを送ったユーザー + interests Interest[] } -// プロフィールの画像。 model Avatar { guid String @id data Bytes } +model Picture { + hash String @id + data Bytes + key String // password +} + +model InterestSubject { + id Int @id @default(autoincrement()) + name String + group String // such as Computer Science | name = ML + Interest Interest[] // ignore this + + @@unique([name, group]) +} + +// User->Interest->InterestSubject +model Interest { + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade ) + subjectId Int + subject InterestSubject @relation(fields: [subjectId], references: [id], onDelete: Cascade) + + @@unique([userId, subjectId]) +} + // enum Gender { // MALE // FEMALE @@ -102,8 +129,6 @@ enum MatchingStatus { REJECTED } -// TODO: lazy load MessageLog s.t. it doesn't need to be loaded on Overview query. -// https://www.prisma.io/docs/orm/prisma-client/queries/select-fields model SharedRoom { id Int @id @default(autoincrement()) thumbnail String // URL to thumbnail picture @@ -118,6 +143,8 @@ model Message { createdAt DateTime @default(now()) // @readonly edited Boolean @default(false) content String + isPicture Boolean // iff the message is a picture. if true, then content is a url of picture. + read Boolean @default(false) relation Relationship? @relation(fields: [relationId], references: [id], onDelete: Cascade) relationId Int? sharedRoom SharedRoom? @relation(fields: [sharedRoomId], references: [id], onDelete: Cascade) diff --git a/server/prisma/sql/recommend.sql b/server/prisma/sql/recommend.sql index 2d7b3839..ad2ad7a4 100644 --- a/server/prisma/sql/recommend.sql +++ b/server/prisma/sql/recommend.sql @@ -1,33 +1,30 @@ -SELECT recv.id, - (SELECT COUNT(*) FROM "Enrollment" recv_enroll - INNER JOIN "Enrollment" req_enroll - ON recv_enroll."courseId" = req_enroll."courseId" - WHERE recv_enroll."userId" = recv.id - AND req_enroll."userId" = $1) -AS overlap FROM "User" recv +-- $1 = senderId +SELECT + *, + -- course overlap + (SELECT COUNT(1) FROM "Course" course + WHERE EXISTS (SELECT 1 FROM "Enrollment" e WHERE e."courseId" = course.id AND e."userId" = recv.id) + AND EXISTS (SELECT 1 FROM "Enrollment" e WHERE e."courseId" = course.id AND e."userId" = $1) + ) + + -- interest overlap + (SELECT COUNT(1) FROM "InterestSubject" subj + WHERE EXISTS (SELECT 1 FROM "Interest" i WHERE i."subjectId" = subj.id AND i."userId" = recv.id) + AND EXISTS (SELECT 1 FROM "Interest" i WHERE i."subjectId" = subj.id AND i."userId" = $1) + ) AS overlap +FROM "User" recv WHERE recv.id <> $1 AND NOT EXISTS ( - SELECT * FROM "Relationship" rel + SELECT 1 FROM "Relationship" rel WHERE rel."sendingUserId" IN ($1, recv.id) AND rel."receivingUserId" IN ($1, recv.id) - AND status = 'MATCHED' + AND (status = 'MATCHED' OR status = 'REJECTED') ) AND NOT EXISTS ( - SELECT * FROM "Relationship" rel_pd + SELECT 1 FROM "Relationship" rel_pd WHERE rel_pd."sendingUserId" = $1 AND rel_pd."receivingUserId" = recv.id AND status = 'PENDING' ) ORDER BY overlap DESC LIMIT $2 OFFSET $3; - --- SELECT recv.id AS recv, COUNT(recv_enroll) AS overlap FROM "User" recv --- LEFT JOIN "Relationship" rel ON (rel."sendingUserId" = recv.id AND rel."receivingUserId" = $1) OR (rel."sendingUserId" = $1 AND rel."sendingUserId" = recv.id) --- LEFT JOIN "Enrollment" recv_enroll ON recv_enroll."userId" = recv.id --- INNER JOIN "Course" course ON recv_enroll."courseId" = course.id --- INNER JOIN "Enrollment" req_enroll ON req_enroll."courseId" = course.id --- WHERE req_enroll."userId" = $1 AND recv.id <> $1 --- AND rel.status != 'MATCHED' --- GROUP BY recv.id --- ORDER BY overlap DESC LIMIT $2 OFFSET $3; diff --git a/server/src/database/chat.ts b/server/src/database/chat.ts index ea35a52e..210dd7e5 100644 --- a/server/src/database/chat.ts +++ b/server/src/database/chat.ts @@ -1,5 +1,4 @@ -import { Err, Ok, type Result } from "../common/lib/result"; -import type { UserID } from "../common/types"; +import type { UserID } from "common/types"; import type { DMOverview, DMRoom, @@ -11,60 +10,116 @@ import type { ShareRoomID, SharedRoom, SharedRoomOverview, -} from "../common/types"; +} from "common/types"; import { prisma } from "./client"; import { getRelation } from "./matches"; -import { getMatchedUser } from "./requests"; +import { + getMatchedUser, + getPendingRequestsFromUser, + getPendingRequestsToUser, +} from "./requests"; +import { getUserByID } from "./users"; -// ユーザーの参加しているすべての Room の概要 (Overview) の取得 -export async function getOverview( - user: UserID, -): Promise> { - try { - const matched = await getMatchedUser(user); - if (!matched.ok) return Err(matched.error); +export async function getOverview(user: UserID): Promise { + const matched = await getMatchedUser(user); + const senders = await getPendingRequestsToUser(user); + const receivers = await getPendingRequestsFromUser(user); - const dm = await Promise.all( - matched.value.map(async (friend) => { - const lastMessageResult = await getLastMessage(user, friend.id); - const lastMessage = lastMessageResult.ok - ? lastMessageResult.value - : undefined; - const overview: DMOverview = { - isDM: true, - friendId: friend.id, - name: friend.name, - thumbnail: friend.pictureUrl, - lastMsg: lastMessage, - }; - return overview; - }), - ); + //マッチングしている人のオーバービュー + const matchingOverview = await Promise.all( + matched.map(async (m) => getOverviewBetween(user, m.id)), + ); - const sharedRooms: { - id: number; - name: string; - thumbnail: string; - }[] = await prisma.sharedRoom.findMany({ - where: { - members: { - has: user, + //自分にリクエストを送ってきた人のオーバービュー + const senderOverview = await Promise.all( + senders.map((s) => getOverviewBetween(user, s.id)), + ); + + //自分がリクエストを送った人のオーバービュー + const receiverOverview = await Promise.all( + receivers.map((r) => getOverviewBetween(user, r.id)), + ); + + const sharedRooms: { + id: number; + name: string; + thumbnail: string; + }[] = await prisma.sharedRoom.findMany({ + where: { + members: { + has: user, + }, + }, + }); + const shared = sharedRooms.map((room) => { + const overview: SharedRoomOverview = { + roomId: room.id as ShareRoomID, + name: room.name, + thumbnail: room.thumbnail, + isDM: false, + }; + return overview; + }); + + const overview = [ + ...matchingOverview, + ...senderOverview, + ...receiverOverview, + ...shared, + ]; + + const sortedOverviewByTime = overview.sort((a, b) => { + const dateA = a.lastMsg?.createdAt ? a.lastMsg.createdAt.getTime() : 0; + const dateB = b.lastMsg?.createdAt ? b.lastMsg.createdAt.getTime() : 0; + return dateB - dateA; + }); + + return [...sortedOverviewByTime]; +} + +async function getOverviewBetween( + user: number, + other: number, +): Promise { + const rel = await getRelation(user, other); + + const friendId = + rel.receivingUserId === user ? rel.sendingUserId : rel.receivingUserId; + const lastMessage = getLastMessage(user, friendId); + const unreadCount = unreadMessages(user, rel.id); + const friend = await getUserByID(friendId); + const overview: DMOverview = { + isDM: true, + matchingStatus: "matched", + friendId: friendId, + name: friend.name, + thumbnail: friend.pictureUrl, + lastMsg: await lastMessage, + unreadMessages: await unreadCount, + }; + return overview; +} +export async function markAsRead( + rel: RelationshipID, + reader: UserID, + message: MessageID, +) { + return await prisma.message.updateMany({ + where: { + id: { + lte: message, + }, + relationId: rel, + creator: { + not: { + equals: reader, }, }, - }); - const shared = sharedRooms.map((room) => { - const overview: SharedRoomOverview = { - roomId: room.id as ShareRoomID, - name: room.name, - thumbnail: room.thumbnail, - isDM: false, - }; - return overview; - }); - return Ok([...shared, ...dm]); - } catch (e) { - return Err(e); - } + }, + data: { + read: true, + }, + }); } /** @@ -73,239 +128,218 @@ export async function getOverview( **/ export async function sendDM( relation: RelationshipID, - content: Omit, -): Promise> { - try { - const message = await prisma.message.create({ - data: { - relationId: relation, - ...content, - }, - }); - return Ok(message); - } catch (e) { - return Err(e); - } + content: Omit, "isPicture">, +): Promise { + const message = await prisma.message.create({ + data: { + // isPicture: false, // todo: bring it back + relationId: relation, + isPicture: false, + read: false, + ...content, + }, + }); + return message; +} +/** +this doesn't create the image. use uploadPic in database/picture.ts to create the image. +**/ +export async function createImageMessage( + sender: UserID, + relation: RelationshipID, + url: string, +) { + return prisma.message.create({ + data: { + creator: sender, + relationId: relation, + content: url, + isPicture: true, + }, + }); } -export async function createSharedRoom( - room: InitRoom, -): Promise> { - try { - type CreateRoom = Omit, "messages">; - const created: CreateRoom = await prisma.sharedRoom.create({ - data: { - thumbnail: "todo", - name: room.name, - members: room.members, - }, - }); - return Ok({ - isDM: false, - messages: [], - ...created, - }); - } catch (e) { - return Err(e); - } +export async function createSharedRoom(room: InitRoom): Promise { + type CreateRoom = Omit, "messages">; + const created: CreateRoom = await prisma.sharedRoom.create({ + data: { + thumbnail: "todo", + name: room.name, + members: room.members, + }, + }); + return { + isDM: false, + messages: [], + ...created, + }; } export async function isUserInRoom( roomId: ShareRoomID, userId: UserID, -): Promise> { - try { - const room = await prisma.sharedRoom.findUnique({ - where: { - id: roomId, - members: { - has: userId, - }, +): Promise { + const room = await prisma.sharedRoom.findUnique({ + where: { + id: roomId, + members: { + has: userId, }, - }); + }, + }); - return Ok(room !== null); - } catch (e) { - return Err(e); - } + return room !== null; } export async function updateRoom( roomId: ShareRoomID, newRoom: Partial>, -): Promise>> { - try { - type UpdatedRoom = Omit, "messages">; - const updated: UpdatedRoom = await prisma.sharedRoom.update({ - where: { - id: roomId, - }, - data: newRoom, - }); - return Ok({ - isDM: false, - ...updated, - }); - } catch (e) { - return Err(e); - } +): Promise> { + type UpdatedRoom = Omit, "messages">; + const updated: UpdatedRoom = await prisma.sharedRoom.update({ + where: { + id: roomId, + }, + data: newRoom, + }); + return { + isDM: false, + ...updated, + }; } export async function inviteUserToSharedRoom( roomId: ShareRoomID, invite: UserID[], -): Promise>> { - try { - const update = await prisma.sharedRoom.update({ - where: { - id: roomId, - }, - data: { - members: { - push: invite, - }, +): Promise> { + const update = await prisma.sharedRoom.update({ + where: { + id: roomId, + }, + data: { + members: { + push: invite, }, - }); - return Ok({ - isDM: false, - ...update, - }); - } catch (e) { - return Err(e); - } + }, + }); + return { + isDM: false, + ...update, + }; } -export async function getDMbetween( - u1: UserID, - u2: UserID, -): Promise> { - try { - const rel = await getRelation(u1, u2); - if (!rel.ok) return Err("room not found"); - - return await findDM(rel.value.id); - } catch (e) { - return Err(e); - } +export async function getDMbetween(u1: UserID, u2: UserID): Promise { + const rel = await getRelation(u1, u2); + return await findDM(rel.id); } -export async function findDM(relID: RelationshipID): Promise> { - try { - const messages: Message[] = await prisma.message.findMany({ - where: { - relationId: relID, - }, - orderBy: { - createdAt: "asc", - }, - }); +export async function findDM(relID: RelationshipID): Promise { + const messages: Message[] = await prisma.message.findMany({ + where: { + relationId: relID, + }, + orderBy: { + createdAt: "asc", + }, + }); - return Ok({ - isDM: true, - id: relID, - messages: messages, - }); - } catch (e) { - return Err(e); - } + return { + isDM: true, + id: relID, + messages: messages, + }; } -export async function getSharedRoom( - roomId: ShareRoomID, -): Promise> { - try { - const room = await prisma.sharedRoom.findUnique({ - where: { - id: roomId, - }, - }); - if (!room) return Err("room not found"); +export async function getSharedRoom(roomId: ShareRoomID): Promise { + const room = await prisma.sharedRoom.findUnique({ + where: { + id: roomId, + }, + }); + if (!room) throw new Error("room not found"); - const messages = await prisma.message.findMany({ - where: { - sharedRoomId: room.id, - }, - }); - return Ok({ - isDM: false, - messages: messages, - ...room, - }); - } catch (e) { - return Err(e); - } + const messages = await prisma.message.findMany({ + where: { + sharedRoomId: room.id, + }, + }); + return { + isDM: false, + messages: messages, + ...room, + }; } -export async function getMessage(id: MessageID): Promise> { - try { - const message = await prisma.message.findUnique({ - where: { - id: id, - }, - }); - if (!message) return Err("message not found"); - return Ok(message); - } catch (e) { - return Err(e); - } +export async function getMessage(id: MessageID): Promise { + const message = await prisma.message.findUnique({ + where: { + id: id, + }, + }); + if (!message) throw new Error("not found"); + return message; } export async function updateMessage( id: MessageID, content: string, -): Promise> { - try { - const message = await prisma.message.update({ - where: { - id: id, - }, - data: { - content: content, - edited: true, - }, - }); - return Ok(message); - } catch (e) { - return Err(e); - } +): Promise { + const message = await prisma.message.update({ + where: { + id: id, + }, + data: { + content: content, + edited: true, + }, + }); + return message; } export async function deleteMessage( id: MessageID, creatorId: UserID | undefined, ): Promise { - try { - const message = await prisma.message.delete({ - where: { - id: id, - creator: creatorId, - }, - }); - return message; - } catch (e) { - return null; - } + const message = await prisma.message.delete({ + where: { + id: id, + creator: creatorId, + }, + }); + return message; } export async function getLastMessage( userId: UserID, friendId: UserID, -): Promise> { - try { - const rel = await getRelation(userId, friendId); - if (!rel.ok) return Err("relation not found"); // relation not found - const lastMessage = await prisma.message.findFirst({ - where: { - relationId: rel.value.id, - }, - orderBy: { - id: "desc", +): Promise { + const rel = await getRelation(userId, friendId); + if (!rel) throw new Error("relation not found"); // relation not found + const lastMessage = await prisma.message.findFirst({ + where: { + relationId: rel.id, + }, + orderBy: { + id: "desc", + }, + take: 1, + }); + return lastMessage ?? undefined; +} + +// only works on Relationship (= DM) for now. +export async function unreadMessages(userId: UserID, roomId: RelationshipID) { + // FIXME: this makes request twice to the database. it's not efficient. + const unreadMessages = await prisma.message.count({ + where: { + read: false, + relationId: roomId, + creator: { + not: { + equals: userId, + }, }, - take: 1, - }); - if (!lastMessage) return Err("last message not found"); - return Ok(lastMessage); - } catch (e) { - return Err(e); - } + }, + }); + return unreadMessages; } diff --git a/server/src/database/client.ts b/server/src/database/client.ts index 27ef828d..7c176ee9 100644 --- a/server/src/database/client.ts +++ b/server/src/database/client.ts @@ -1,9 +1,37 @@ import { PrismaClient } from "@prisma/client"; +import { format } from "sql-formatter"; import "../load-env"; -export let prisma = new PrismaClient(); +const { SQL_LOG } = process.env; -// not sure if this is necessary -export function reload() { - prisma = new PrismaClient(); -} +export const prisma = new PrismaClient( + SQL_LOG === "true" + ? { + log: [ + { + emit: "event", + level: "query", + }, + { + emit: "stdout", + level: "error", + }, + { + emit: "stdout", + level: "info", + }, + { + emit: "stdout", + level: "warn", + }, + ], + } + : undefined, +); + +prisma.$on("query", (e) => { + console.log(format(e.query, { language: "postgresql" })); + console.log(`Params: ${e.params}`); + console.log(`Duration: ${e.duration}`); + console.log("\n"); +}); diff --git a/server/src/database/courses.ts b/server/src/database/courses.ts index 5ddc0c48..edc2fe38 100644 --- a/server/src/database/courses.ts +++ b/server/src/database/courses.ts @@ -1,17 +1,18 @@ -import type { Course, Day, UserID } from "../common/types"; +import type { Course, Day, UserID } from "common/types"; +import { error } from "../lib/error"; import { prisma } from "./client"; -export async function getCourseByCourseId( - courseId: string, -): Promise { - return await prisma.course.findUnique({ - where: { - id: courseId, - }, - include: { - slots: true, - }, - }); +export async function getCourseByCourseId(courseId: string): Promise { + return ( + (await prisma.course.findUnique({ + where: { + id: courseId, + }, + include: { + slots: true, + }, + })) ?? error("course not found", 404) + ); } /** diff --git a/server/src/database/enrollments.ts b/server/src/database/enrollments.ts index a2ea0a36..49075b9a 100644 --- a/server/src/database/enrollments.ts +++ b/server/src/database/enrollments.ts @@ -1,4 +1,4 @@ -import type { Course, CourseID, UserID } from "../common/types"; +import type { Course, CourseID, UserID } from "common/types"; import { prisma } from "./client"; import { getCoursesByUserId } from "./courses"; diff --git a/server/src/database/interest.test.ts b/server/src/database/interest.test.ts new file mode 100644 index 00000000..b9a22b25 --- /dev/null +++ b/server/src/database/interest.test.ts @@ -0,0 +1,25 @@ +import { beforeAll, expect, test } from "bun:test"; +import { assertLocalDB } from "../load-env"; +import * as interest from "./interest"; + +beforeAll(assertLocalDB); + +test("list", async () => { + const got = (await interest.all()).sort((a, b) => a.id - b.id); + expect(got).toEqual([ + { id: 1, group: "Computer Science", name: "型システム" }, + { id: 2, group: "Computer Science", name: "機械学習" }, + { id: 3, group: "Computer Science", name: "CPU アーキテクチャ" }, + { id: 4, group: "Computer Science", name: "分散処理" }, + { id: 5, group: "Math", name: "Lean4" }, + ]); +}); + +test("get by user id", async () => { + const got = (await interest.of(101)).sort((a, b) => a.id - b.id); + expect(got).toEqual([ + { id: 1, group: "Computer Science", name: "型システム" }, + { id: 2, group: "Computer Science", name: "機械学習" }, + { id: 3, group: "Computer Science", name: "CPU アーキテクチャ" }, + ]); +}); diff --git a/server/src/database/interest.ts b/server/src/database/interest.ts new file mode 100644 index 00000000..00e5f708 --- /dev/null +++ b/server/src/database/interest.ts @@ -0,0 +1,78 @@ +import type { InterestSubject, UserID } from "common/types"; +import { error } from "../lib/error"; +import { prisma } from "./client"; + +export async function all(): Promise { + return await prisma.interestSubject.findMany(); +} + +export async function get(id: number): Promise { + return ( + (await prisma.interestSubject.findUnique({ where: { id } })) ?? + error("not found", 404) + ); +} + +export async function create(name: string): Promise { + const existingTag = await prisma.interestSubject.findMany({ + where: { name }, + }); + if (existingTag.length > 0) { + error("同名のタグがすでに存在します", 409); + } + return await prisma.interestSubject.create({ + data: { + name, + group: "", // TODO: 運用次第 + }, + }); +} + +export async function of(userId: UserID): Promise { + return await prisma.interest + .findMany({ + where: { + userId, + }, + select: { + subject: true, + }, + }) + .then((res) => res.map((interest) => interest.subject)); +} + +export async function add(userId: UserID, subjectId: number) { + return await prisma.interest.create({ + data: { + userId, + subjectId, + }, + }); +} +export async function remove(userId: UserID, subjectId: number) { + return await prisma.interest.delete({ + where: { + userId_subjectId: { userId, subjectId }, + }, + }); +} + +export async function updateMultipleWithTransaction( + userId: UserID, + subjectIds: number[], +) { + return await prisma.$transaction(async (prisma) => { + await prisma.interest.deleteMany({ + where: { + userId, + }, + }); + + await prisma.interest.createMany({ + data: subjectIds.map((subjectId) => ({ + userId, + subjectId, + })), + }); + }); +} diff --git a/server/src/database/matches.ts b/server/src/database/matches.ts index 4b8aec72..b5f3ed72 100644 --- a/server/src/database/matches.ts +++ b/server/src/database/matches.ts @@ -1,140 +1,116 @@ -import { Err, Ok, type Result } from "../common/lib/result"; -import type { Relationship, UserID } from "../common/types"; +import { panic } from "common/lib/panic"; +import type { Relationship, UserID } from "common/types"; import asyncMap from "../lib/async/map"; +import { error } from "../lib/error"; import { prisma } from "./client"; export async function getRelation( u1: UserID, u2: UserID, -): Promise> { - try { - // TODO!!!! FIXME!!!!!! FIX THIS findMany!!!!! - const rel = await prisma.relationship.findMany({ - where: { - OR: [ - { sendingUserId: u1, receivingUserId: u2 }, - { sendingUserId: u2, receivingUserId: u1 }, - ], - }, - }); - return rel[0] ? Ok(rel[0]) : Err(404); - } catch (e) { - return Err(e); - } +): Promise { + // FIXME: fix this findMany + const rel = await prisma.relationship.findMany({ + where: { + OR: [ + { sendingUserId: u1, receivingUserId: u2 }, + { sendingUserId: u2, receivingUserId: u1 }, + ], + }, + }); + return rel[0] ?? panic("not found"); } -export async function getRelations( - user: UserID, -): Promise> { - try { - const relations: Relationship[] = await prisma.relationship.findMany({ - where: { - OR: [{ sendingUserId: user }, { receivingUserId: user }], - }, - }); - return Ok(relations); - } catch (e) { - return Err(e); - } +export async function getRelations(user: UserID): Promise { + const relations: Relationship[] = await prisma.relationship.findMany({ + where: { + OR: [{ sendingUserId: user }, { receivingUserId: user }], + }, + }); + return relations; } // returns false if u1 or u2 is not present. export async function areMatched(u1: UserID, u2: UserID): Promise { const match = await getRelation(u1, u2); - if (!match.ok) return false; + if (!match) return false; - return match.value.status === "MATCHED"; + return match.status === "MATCHED"; } export async function areAllMatched( user: UserID, friends: UserID[], -): Promise> { - try { - return Ok( - ( - await asyncMap(friends, (friend) => { - return areMatched(user, friend); - }) - ).reduce((a, b) => a && b), - ); - } catch (e) { - return Err(e); - } +): Promise { + return ( + await asyncMap(friends, (friend) => { + return areMatched(user, friend); + }) + ).reduce((a, b) => a && b); } // 特定のユーザIDを含むマッチの取得 export async function getMatchesByUserId( userId: UserID, -): Promise> { - try { - const m = await prisma.relationship.findMany({ - where: { - AND: [ - { status: "MATCHED" }, - { OR: [{ sendingUserId: userId }, { receivingUserId: userId }] }, - ], - }, - }); - return Ok(m); - } catch (e) { - return Err(e); - } +): Promise { + return await prisma.relationship.findMany({ + where: { + AND: [ + { status: "MATCHED" }, + { OR: [{ sendingUserId: userId }, { receivingUserId: userId }] }, + ], + }, + }); } // マッチの削除 export async function deleteMatch( senderId: UserID, receiverId: UserID, -): Promise> { - try { - // 最初の条件で削除を試みる - const recordToDelete = await prisma.relationship.findUnique({ +): Promise { + // 最初の条件で削除を試みる + const recordToDelete = await prisma.relationship.findUnique({ + where: { + sendingUserId_receivingUserId: { + sendingUserId: senderId, + receivingUserId: receiverId, + }, + }, + }); + + if (recordToDelete) { + await prisma.relationship.update({ where: { - sendingUserId_receivingUserId: { - sendingUserId: senderId, - receivingUserId: receiverId, - }, + id: recordToDelete.id, + }, + data: { + status: "REJECTED", }, }); + return; + } - if (recordToDelete) { - await prisma.relationship.update({ - where: { - id: recordToDelete.id, - }, - data: { - status: "REJECTED", - }, - }); - return Ok(undefined); - } + // 次の条件で削除を試みる + const altRecordToDelete = await prisma.relationship.findUnique({ + where: { + sendingUserId_receivingUserId: { + sendingUserId: receiverId, + receivingUserId: senderId, + }, + }, + }); - // 次の条件で削除を試みる - const altRecordToDelete = await prisma.relationship.findUnique({ + if (altRecordToDelete) { + await prisma.relationship.update({ where: { - sendingUserId_receivingUserId: { - sendingUserId: receiverId, - receivingUserId: senderId, - }, + id: altRecordToDelete.id, + }, + data: { + status: "REJECTED", }, }); - - if (altRecordToDelete) { - await prisma.relationship.update({ - where: { - id: altRecordToDelete.id, - }, - data: { - status: "REJECTED", - }, - }); - return Ok(undefined); - } - - // `No matching records found for senderId=${senderId} and receiverId=${receiverId}`, - return Err(404); - } catch (e) { - return Err(e); + return; } + + // `No matching records found for senderId=${senderId} and receiverId=${receiverId}`, + error("not found", 404); } diff --git a/server/src/database/picture.ts b/server/src/database/picture.ts index d4c39181..b29a7226 100644 --- a/server/src/database/picture.ts +++ b/server/src/database/picture.ts @@ -1,40 +1,61 @@ -import { Err, Ok, type Result } from "../common/lib/result"; -import type { GUID } from "../common/types"; +import { panic } from "common/lib/panic"; +import type { GUID } from "common/types"; import { prisma } from "./client"; +/** + * @returns URL of the uploaded file. + * @throws on database conn fail. + **/ +export async function uploadPic( + hash: string, + buf: Buffer, + passkey: string, +): Promise { + await prisma.picture.upsert({ + where: { hash }, + create: { hash, data: buf, key: passkey }, + update: { data: buf, key: passkey }, + }); + const url = `/picture/${hash}?key=${passkey}`; + return url; +} + +export async function getPic(hash: string, passkey: string) { + return prisma.picture + .findUnique({ + where: { + hash, + key: passkey, + }, + }) + .then((val) => val?.data); +} + /** * is safe to await. * @returns URL of the file. **/ -export async function set(guid: GUID, buf: Buffer): Promise> { +export async function setProf(guid: GUID, buf: Buffer): Promise { return prisma.avatar .upsert({ where: { - guid: guid, + guid, }, create: { guid, data: buf }, update: { data: buf }, }) .then(() => { // ?update=${date} is necessary to let the browsers properly cache the image. - const pictureUrl = `/picture/${guid}?update=${new Date().getTime()}`; - return Ok(pictureUrl); - }) - .catch((err) => { - console.error("Error uploading file:", err); - return Err(err); + const pictureUrl = `/picture/profile/${guid}?update=${new Date().getTime()}`; + return pictureUrl; }); } // is await-safe. -export async function get(guid: GUID): Promise> { +export async function getProf(guid: GUID): Promise { return prisma.avatar .findUnique({ where: { guid }, }) - .then((res) => (res ? Ok(res.data) : Err(404))) - .catch((err) => { - console.log("Error reading db: ", err); - return Err(err); - }); + .then((res) => res?.data ?? panic("not found")); } diff --git a/server/src/database/requests.ts b/server/src/database/requests.ts index 780de373..5efd922a 100644 --- a/server/src/database/requests.ts +++ b/server/src/database/requests.ts @@ -1,5 +1,9 @@ -import { Err, Ok, type Result } from "../common/lib/result"; -import type { Relationship, User, UserID } from "../common/types"; +import { panic } from "common/lib/panic"; +import type { + Relationship, + UserID, + UserWithCoursesAndSubjects, +} from "common/types"; import { prisma } from "./client"; // マッチリクエストの送信 @@ -9,177 +13,259 @@ export async function sendRequest({ }: { senderId: UserID; receiverId: UserID; -}): Promise> { +}): Promise { // 既存の関係をチェック - try { - // TODO: fix this findFirst to be findUnique - const existingRelationship = await prisma.relationship.findFirst({ - where: { - OR: [ - { sendingUserId: senderId, receivingUserId: receiverId }, - { sendingUserId: receiverId, receivingUserId: senderId }, // 逆の関係もチェック - ], - }, - }); - // 既存の関係がある場合はそのまま返す - if (existingRelationship) { - // 相手がすでにこちらに Request を送っている場合は approve (accept) したとみなす - if (existingRelationship.receivingUserId === senderId) - approveRequest( - existingRelationship.sendingUserId, - existingRelationship.receivingUserId, - ); - return Ok(existingRelationship); - } - const newRelationship = await prisma.relationship.create({ - data: { - sendingUser: { connect: { id: senderId } }, - receivingUser: { connect: { id: receiverId } }, - status: "PENDING", - }, - }); - return Ok(newRelationship); - } catch (e) { - return Err(e); + // TODO: fix this findFirst to be findUnique + const existingRelationship = await prisma.relationship.findFirst({ + where: { + OR: [ + { sendingUserId: senderId, receivingUserId: receiverId }, + { sendingUserId: receiverId, receivingUserId: senderId }, // 逆の関係もチェック + ], + }, + }); + // 既存の関係がある場合はそのまま返す + if (existingRelationship) { + // 相手がすでにこちらに Request を送っている場合は approve (accept) したとみなす + if (existingRelationship.receivingUserId === senderId) + approveRequest( + existingRelationship.sendingUserId, + existingRelationship.receivingUserId, + ); + return existingRelationship; } + const newRelationship = await prisma.relationship.create({ + data: { + sendingUser: { connect: { id: senderId } }, + receivingUser: { connect: { id: receiverId } }, + status: "PENDING", + }, + }); + return newRelationship; } // マッチリクエストの承認 export async function approveRequest( senderId: UserID, receiverId: UserID, -): Promise> { - try { - const updated = await prisma.relationship.update({ - where: { - sendingUserId_receivingUserId: { - sendingUserId: senderId, - receivingUserId: receiverId, - }, - }, - data: { - status: "MATCHED", +): Promise { + const updated = await prisma.relationship.update({ + where: { + sendingUserId_receivingUserId: { + sendingUserId: senderId, + receivingUserId: receiverId, }, - }); - return updated === null ? Err(404) : Ok(updated); - } catch (e) { - return Err(e); - } + }, + data: { + status: "MATCHED", + }, + }); + return updated === null ? panic("not found") : updated; } // マッチリクエストの拒否 export async function rejectRequest( senderId: UserID, receiverId: UserID, -): Promise> { - try { - const rel = await prisma.relationship.update({ - where: { - sendingUserId_receivingUserId: { - sendingUserId: senderId, - receivingUserId: receiverId, - }, - }, - data: { - status: "REJECTED", +): Promise { + const rel = await prisma.relationship.update({ + where: { + sendingUserId_receivingUserId: { + sendingUserId: senderId, + receivingUserId: receiverId, }, - }); - return rel === null ? Err(404) : Ok(rel); - } catch (e) { - return Err(e); - } + }, + data: { + status: "REJECTED", + }, + }); + return rel === null ? panic("not found") : rel; } export async function cancelRequest( senderId: UserID, receiverId: UserID, -): Promise> { - try { - return prisma.relationship - .delete({ - where: { - sendingUserId_receivingUserId: { - sendingUserId: senderId, - receivingUserId: receiverId, - }, +): Promise { + return prisma.relationship + .delete({ + where: { + sendingUserId_receivingUserId: { + sendingUserId: senderId, + receivingUserId: receiverId, }, - }) - .then(() => Ok(undefined)) - .catch((err) => Err(err)); - } catch (err) { - return Err(err); - } + }, + }) + .then(); } //ユーザーへのリクエストを探す 俺をリクエストしているのは誰だ export async function getPendingRequestsToUser( userId: UserID, -): Promise> { - try { - const found = await prisma.user.findMany({ - where: { - sendingUsers: { - some: { - receivingUserId: userId, - status: "PENDING", +): Promise { + const found = await prisma.user.findMany({ + where: { + sendingUsers: { + some: { + receivingUserId: userId, + status: "PENDING", + }, + }, + }, + include: { + enrollments: { + include: { + course: { + include: { + slots: true, + }, }, }, }, - }); - return Ok(found); - } catch (e) { - return Err(e); - } + interests: { + include: { + subject: true, + }, + }, + }, + }); + return found.map((user) => { + return { + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; + }), + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), + }; + }); } //ユーザーがリクエストしている人を探す 俺がリクエストしているのは誰だ export async function getPendingRequestsFromUser( userId: UserID, -): Promise> { - try { - const found = await prisma.user.findMany({ - where: { - receivingUsers: { - some: { - sendingUserId: userId, - status: "PENDING", +): Promise { + const found = await prisma.user.findMany({ + where: { + receivingUsers: { + some: { + sendingUserId: userId, + status: "PENDING", + }, + }, + }, + include: { + enrollments: { + include: { + course: { + include: { + enrollments: true, + slots: true, + }, }, }, }, - }); - return Ok(found); - } catch (e) { - return Err(e); - } + interests: { + include: { + subject: true, + }, + }, + }, + }); + return found.map((user) => { + return { + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; + }), + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), + }; + }); } //マッチした人の取得 -export async function getMatchedUser(userId: UserID): Promise> { - try { - const found = await prisma.user.findMany({ - where: { - OR: [ - { - sendingUsers: { - some: { - receivingUserId: userId, - status: "MATCHED", - }, +export async function getMatchedUser( + userId: UserID, +): Promise { + const found = await prisma.user.findMany({ + where: { + OR: [ + { + sendingUsers: { + some: { + receivingUserId: userId, + status: "MATCHED", }, }, - { - receivingUsers: { - some: { - sendingUserId: userId, - status: "MATCHED", - }, + }, + { + receivingUsers: { + some: { + sendingUserId: userId, + status: "MATCHED", }, }, - ], + }, + ], + }, + include: { + enrollments: { + include: { + course: { + include: { + enrollments: true, + slots: true, + }, + }, + }, }, - }); - return Ok(found); - } catch (e) { - return Err(e); - } + interests: { + include: { + subject: true, + }, + }, + }, + }); + return found.map((user) => { + return { + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; + }), + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), + }; + }); +} + +export async function getMatchedRelations( + userId: UserID, +): Promise { + const found = await prisma.relationship.findMany({ + where: { + status: "MATCHED", + OR: [ + { + sendingUserId: userId, + }, + { + receivingUserId: userId, + }, + ], + }, + }); + return found; +} + +export async function matchWithMemo(userId: UserID) { + await prisma.relationship.create({ + data: { + status: "MATCHED", + sendingUserId: userId, + receivingUserId: 0, //KeepメモのUserId + }, + }); } diff --git a/server/src/database/users.ts b/server/src/database/users.ts index 0ee23e0c..6428a5b3 100644 --- a/server/src/database/users.ts +++ b/server/src/database/users.ts @@ -1,146 +1,204 @@ -import { Err, Ok, type Result } from "../common/lib/result"; -import type { GUID, UpdateUser, User, UserID } from "../common/types"; +import { panic } from "common/lib/panic"; +import { error } from "../lib/error"; + +import type { + Course, + GUID, + InterestSubject, + UpdateUser, + User, + UserID, + UserWithCoursesAndSubjects, +} from "common/types"; import { prisma } from "./client"; // ユーザーの作成 -export async function createUser( - partialUser: Omit, -): Promise> { - try { - const newUser = await prisma.user.create({ - data: partialUser, - }); - return Ok(newUser); - } catch (e) { - return Err(e); - } +export async function createUser(partialUser: Omit): Promise { + const newUser = await prisma.user.create({ + data: partialUser, + }); + return newUser; } // ユーザーの取得 -export async function getUser(guid: GUID): Promise> { - try { - const user = await prisma.user.findUnique({ - where: { - guid: guid, +export async function getUser(guid: GUID): Promise { + const user = await prisma.user.findUnique({ + where: { + guid: guid, + }, + include: { + enrollments: { + include: { + course: { + include: { + slots: true, + }, + }, + }, }, - }); - if (!user) return Err(404); - return Ok(user); - } catch (e) { - return Err(e); - } + interests: { + include: { + subject: true, + }, + }, + }, + }); + if (!user) error("not found", 404); + return { + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; + }), + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), + }; } -export async function getGUIDByUserID(id: UserID): Promise> { +export async function getGUIDByUserID(id: UserID): Promise { return prisma.user .findUnique({ where: { id }, select: { guid: true }, }) - .then((v) => (v ? Ok(v.guid) : Err(404))) - .catch((e) => Err(e)); + .then((v) => v?.guid ?? panic("not found")); } -export async function getUserIDByGUID(guid: GUID): Promise> { +export async function getUserIDByGUID(guid: GUID): Promise { return prisma.user .findUnique({ where: { guid }, select: { id: true }, }) - .then((res) => res?.id) - .then((id) => (id ? Ok(id) : Err(404))) - .catch((err) => Err(err)); + .then((res) => res?.id ?? panic("not found")); } -export async function getUserByID(id: UserID): Promise> { - try { - const user = await prisma.user.findUnique({ - where: { - id, +export async function getUserByID( + id: UserID, +): Promise { + const user = await prisma.user.findUnique({ + where: { + id, + }, + include: { + enrollments: { + include: { + course: { + include: { + slots: true, + }, + }, + }, + }, + interests: { + include: { + subject: true, + }, }, - }); - return user === null ? Err(404) : Ok(user); - } catch (e) { - return Err(e); - } + }, + }); + if (!user) throw new Error("not found"); + return { + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; + }), + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), + }; } // ユーザーの更新 export async function updateUser( userId: UserID, partialUser: Partial, -): Promise> { +): Promise { // undefined means do nothing to this field // https://www.prisma.io/docs/orm/prisma-client/special-fields-and-types/null-and-undefined#use-case-null-and-undefined-in-a-graphql-resolver - try { - if (!partialUser.pictureUrl) partialUser.pictureUrl = undefined; // don't delete picture if not provided - const updateUser = { - id: undefined, - guid: undefined, - ...partialUser, - }; - const updatedUser = await prisma.user.update({ - where: { id: userId }, - data: updateUser, - }); - return updatedUser === null ? Err(404) : Ok(updatedUser); - } catch (e) { - return Err(e); - } + if (!partialUser.pictureUrl) partialUser.pictureUrl = undefined; // don't delete picture if not provided + const updateUser = { + id: undefined, + guid: undefined, + ...partialUser, + }; + const updatedUser = await prisma.user.update({ + where: { id: userId }, + data: updateUser, + }); + return updatedUser ?? panic("not found"); } // ユーザーの削除 -export async function deleteUser(userId: UserID): Promise> { - try { - const deletedUser = await prisma.user.delete({ - where: { id: userId }, - }); - return deletedUser === null ? Err(404) : Ok(deletedUser); - } catch (e) { - return Err(e); - } +export async function deleteUser(userId: UserID): Promise { + const deletedUser = await prisma.user.delete({ + where: { id: userId }, + }); + return deletedUser ?? panic("not found"); } // ユーザーの全取得 -export async function getAllUsers(): Promise> { - try { - const users = await prisma.user.findMany(); - return Ok(users); - } catch (e) { - return Err(e); - } +export async function getAllUsers(): Promise< + (User & { courses: Course[]; interestSubjects: InterestSubject[] })[] +> { + const users = await prisma.user.findMany({ + include: { + enrollments: { + include: { + course: { + include: { + slots: true, + }, + }, + }, + }, + interests: { + include: { + subject: true, + }, + }, + }, + }); + return users.map((user) => { + return { + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; + }), + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), + }; + }); } // TODO: FIXME: currently also showing users that the requester has already sent request to, to not change behavior. // but this is probably not ideal. consider only showing people with no relation. // (or just remove this function and use recommended() instead) -export async function unmatched(id: UserID): Promise> { - return prisma.user - .findMany({ - where: { - AND: [ - { - receivingUsers: { - none: { - sendingUserId: id, - status: "MATCHED", - }, +export async function unmatched(id: UserID): Promise { + return prisma.user.findMany({ + where: { + AND: [ + { + receivingUsers: { + none: { + sendingUserId: id, + status: "MATCHED", }, }, - { - sendingUsers: { - none: { - receivingUserId: id, - status: "MATCHED", - }, + }, + { + sendingUsers: { + none: { + receivingUserId: id, + status: "MATCHED", }, }, - { - NOT: { - id: id, - }, + }, + { + NOT: { + id: id, }, - ], - }, - }) - .then((res) => Ok(res)) - .catch((err) => Err(err)); + }, + ], + }, + }); } diff --git a/server/src/firebase/auth/db.ts b/server/src/firebase/auth/db.ts index dd9aa507..9f4865a5 100644 --- a/server/src/firebase/auth/db.ts +++ b/server/src/firebase/auth/db.ts @@ -1,25 +1,23 @@ -import { PrismaClient } from "@prisma/client"; -import type { Request } from "express"; -import { Err, Ok, type Result } from "../../common/lib/result"; -import type { IDToken, UserID } from "../../common/types"; +import type { GUID, IDToken, UserID } from "common/types"; +import type { Context } from "hono"; +import { LRUCache } from "lru-cache"; +import { prisma } from "../../database/client"; +import { error } from "../../lib/error"; import { getGUID, getGUIDFromToken } from "./lib"; -import { prisma } from "../../database/client"; -/** - * REQUIRE: cookieParser middleware before this - * THROWS: if idToken is not present in request cookie, or when the token is not valid. - * Expected use case: - * ```js - * let userId: number; - * try { - * userId = await getUserId(req); - * } catch { - * return res.status(401).send("auth error"); - * } - * ``` - **/ -export async function getUserId(req: Request): Promise { - const guid = await getGUID(req); +const guid_userid_cache = new LRUCache({ + max: 100, +}); + +export async function getUserId(c: Context): Promise { + const guid = await getGUID(c); + + const cache = guid_userid_cache.get(guid); + if (cache) { + console.log(`[CACHE HIT] ${guid} -> ${cache}`); + return cache; + } + const user = await prisma.user.findUnique({ where: { guid: guid, @@ -28,39 +26,32 @@ export async function getUserId(req: Request): Promise { id: true, }, }); - if (!user) throw new Error("User not found!"); + if (!user) error("auth error: unauthorized", 401); + guid_userid_cache.set(guid, user.id); return user.id; } export async function getUserIdFromToken(token: IDToken): Promise { const guid = await getGUIDFromToken(token); + + const cache = guid_userid_cache.get(guid); + if (cache) { + return cache; + } + const user = await prisma.user.findUnique({ where: { guid: guid, }, + select: { + id: true, + }, }); - if (!user) throw new Error("User not found!"); + if (!user) error("User not found!", 401); + guid_userid_cache.set(guid, user.id); return user.id; } -/** - * never throws. - * Expected use case: - * ```js - * const result = await safeGetUserId(req); - * if (!result.ok) - * return res.status(401).send("auth error"); - * const userId = result.value; - * ``` - **/ -export async function safeGetUserId(req: Request): Promise> { - try { - return Ok(await getUserId(req)); - } catch (e) { - return Err(e); - } -} - /** returns true if userid is requester's id. * otherwise returns false. * never throws. @@ -72,12 +63,12 @@ export async function safeGetUserId(req: Request): Promise> { ``` **/ export async function isRequester( - req: Request, + c: Context, userid: UserID, ): Promise { - const result = await safeGetUserId(req); - if (!result.ok) return false; - if (result.value !== userid) return false; - - return true; + try { + return (await getUserId(c)) === userid; + } catch (_) { + return false; + } } diff --git a/server/src/firebase/auth/lib.ts b/server/src/firebase/auth/lib.ts index 045a47a4..a7055341 100644 --- a/server/src/firebase/auth/lib.ts +++ b/server/src/firebase/auth/lib.ts @@ -1,7 +1,7 @@ -import type { Request } from "express"; +import type { GUID, IDToken } from "common/types"; import * as admin from "firebase-admin/auth"; -import { Err, Ok, type Result } from "../../common/lib/result"; -import type { GUID, IDToken } from "../../common/types"; +import type { Context } from "hono"; +import { error } from "../../lib/error"; import { app } from "../init"; const auth = admin.getAuth(app); @@ -9,32 +9,17 @@ type DecodedIdToken = admin.DecodedIdToken; // REQUIRE: cookieParser middleware before this // THROWS: if idToken is not present in request cookie, or when the token is not valid. -export async function getGUID(req: Request): Promise { - const idToken = req.query.token; - if (typeof idToken !== "string") throw new Error(); +export async function getGUID(c: Context): Promise { + const idToken = c.req.header("Authorization"); + if (typeof idToken !== "string") error("token not found in header", 401); return await getGUIDFromToken(idToken); } -export let getGUIDFromToken = async (token: IDToken) => { - return (await verifyIDToken(token)).uid as GUID; -}; - -// skip auth in test -if (process.env.UNSAFE_SKIP_AUTH) { - getGUIDFromToken = async (token: IDToken) => { - if (token === "I_AM_abc101") { - return "abc101"; - } - return (await verifyIDToken(token)).uid as GUID; - }; -} - -export async function safeGetGUID(req: Request): Promise> { - try { - return Ok(await getGUID(req)); - } catch (e) { - return Err(e); +export async function getGUIDFromToken(token: IDToken) { + if (process.env.UNSAFE_SKIP_AUTH && token === "I_AM_abc101") { + return "abc101"; } + return (await verifyIDToken(token)).uid as GUID; } export async function verifyIDToken(idToken: IDToken): Promise { diff --git a/server/src/functions/chat.ts b/server/src/functions/chat.ts index e715ca13..3f6a3aad 100644 --- a/server/src/functions/chat.ts +++ b/server/src/functions/chat.ts @@ -1,5 +1,4 @@ -import type { Result } from "../common/lib/result"; -import type { InitRoom, SharedRoom, UserID } from "../common/types"; +import type { InitRoom, SharedRoom, UserID } from "common/types"; import type { DMRoom, Message, @@ -8,73 +7,84 @@ import type { RoomOverview, SendMessage, ShareRoomID, -} from "../common/types"; +} from "common/types"; +import { HTTPException } from "hono/http-exception"; import * as db from "../database/chat"; -import { areAllMatched, areMatched, getRelation } from "../database/matches"; +import { areAllMatched, getRelation } from "../database/matches"; import { getUserByID } from "../database/users"; import * as http from "./share/http"; export async function getOverview( id: number, ): Promise> { - const overview: Result = await db.getOverview(id); - if (!overview.ok) { - console.error(overview.error); + try { + const overview: RoomOverview[] = await db.getOverview(id); + return { + ok: true, + code: 200, + body: overview, + }; + } catch (err) { + console.error(err); return { ok: false, code: 500, - body: overview.error as string, + body: (err as Error).message, }; } - - return { - ok: true, - code: 200, - body: overview.value, - }; - // SEND: RoomOverview[]. - // this is NOT ordered. you need to sort it on frontend. } export async function sendDM( from: UserID, to: UserID, send: SendMessage, -): Promise> { +): Promise { const rel = await getRelation(from, to); - if (!rel.ok || rel.value.status !== "MATCHED") - return http.forbidden("cannot send to non-friend"); + if (rel.status === "REJECTED") + throw new HTTPException(403, { + message: + "You cannot send a message because the friendship request was rejected.", + }); // they are now MATCHED - const msg: Omit = { + const msg: Omit, "isPicture"> = { creator: from, createdAt: new Date(), edited: false, ...send, }; - const result = await db.sendDM(rel.value.id, msg); - if (!result.ok) return http.internalError(""); - return http.created(result.value); + const result = await db.sendDM(rel.id, msg); + if (!result) + throw new HTTPException(500, { message: "Failed to send message" }); + return result; } export async function getDM( - requester: UserID, - _with: UserID, + user: UserID, + friend: UserID, ): Promise> { - if (!areMatched(requester, _with)) - return http.forbidden("cannot DM with a non-friend"); + const rel = await getRelation(user, friend); + if (rel.status === "REJECTED") + return http.forbidden("cannot send to rejected-friend"); - const room = await db.getDMbetween(requester, _with); - if (!room.ok) return http.internalError(); + const room = await db.getDMbetween(user, friend); - const friendData = await getUserByID(_with); - if (!friendData.ok) return http.notFound("friend not found"); + const friendData = await getUserByID(friend); + const unreadCount = db.unreadMessages(user, rel.id); const personalized: PersonalizedDMRoom & DMRoom = { - name: friendData.value.name, - thumbnail: friendData.value.pictureUrl, - ...room.value, + unreadMessages: await unreadCount, + friendId: friendData.id, + name: friendData.name, + thumbnail: friendData.pictureUrl, + matchingStatus: + rel.status === "MATCHED" + ? "matched" + : rel.sendingUserId === user //どっちが送ったリクエストなのかを判定 + ? "myRequest" + : "otherRequest", + ...room, }; return http.ok(personalized); @@ -85,32 +95,25 @@ export async function createRoom( init: InitRoom, ): Promise> { const allMatched = await areAllMatched(creator, init.members); - if (!allMatched.ok) return http.unauthorized("db error"); + if (!allMatched) return http.unauthorized("db error"); - if (!allMatched.value) + if (!allMatched) return http.forbidden("some members are not matched with you"); const room = await db.createSharedRoom(init); - if (!room.ok) return http.internalError("failed to create"); + if (!room) return http.internalError("failed to create"); - return http.created(room.value); + return http.created(room); } export async function getRoom( user: UserID, roomId: ShareRoomID, ): Promise> { - const userInRoom = await db.isUserInRoom(roomId, user); - - if (!userInRoom.ok) return http.internalError("db error"); - if (!userInRoom.value) + if (!(await db.isUserInRoom(roomId, user))) return http.unauthorized("you don't belong to that room"); - const room = await db.getSharedRoom(roomId); - if (!room.ok) return http.internalError(); - if (!room.value) return http.notFound(); - - return http.ok(room.value); + return http.ok(room); } export async function patchRoom( @@ -119,11 +122,8 @@ export async function patchRoom( newRoom: SharedRoom, ): Promise>> { if (!(await db.isUserInRoom(roomId, user))) return http.forbidden(); - const room = await db.updateRoom(roomId, newRoom); - if (!room.ok) return http.internalError(); - - return http.created(room.value); + return http.created(room); } export async function inviteUserToRoom( @@ -133,12 +133,8 @@ export async function inviteUserToRoom( ): Promise>> { if (!(await areAllMatched(requester, invited))) return http.forbidden("some of the members are not friends with you"); - const room = await db.inviteUserToSharedRoom(roomId, invited); - - if (!room.ok) return http.internalError(); - - return http.ok(room.value); + return http.ok(room); } export async function updateMessage( @@ -147,12 +143,8 @@ export async function updateMessage( content: string, ): Promise> { const old = await db.getMessage(messageId as MessageID); - if (!old.ok) return http.notFound("couldn't find message"); - if (old.value.creator !== requester) + if (old.creator !== requester) return http.forbidden("cannot edit others' message"); - const msg = await db.updateMessage(messageId, content); - if (!msg.ok) return http.internalError(); - - return http.ok(msg.value); + return http.ok(msg); } diff --git a/server/src/functions/engines/recommendation.test.ts b/server/src/functions/engines/recommendation.test.ts index 5a046854..78444dd1 100644 --- a/server/src/functions/engines/recommendation.test.ts +++ b/server/src/functions/engines/recommendation.test.ts @@ -8,14 +8,11 @@ beforeAll(() => { test("recommendation engine", async () => { const usersFor101 = await recommendedTo(101, 5, 0); - if (!usersFor101.ok) throw new Error(); - expect(usersFor101.value.map((entry) => entry.u.id)).toEqual([102, 103]); + expect(usersFor101.map((entry) => entry.u.id)).toEqual([102, 103]); const usersFor102 = await recommendedTo(102, 5, 0); - if (!usersFor102.ok) throw new Error(); - expect(usersFor102.value.map((entry) => entry.u.id)).toEqual([103, 101]); + expect(usersFor102.map((entry) => entry.u.id)).toEqual([103, 101]); const usersFor103 = await recommendedTo(103, 5, 0); - if (!usersFor103.ok) throw new Error(); - expect(usersFor103.value.map((entry) => entry.u.id)).toEqual([102, 101]); + expect(usersFor103.map((entry) => entry.u.id)).toEqual([102, 101]); }); diff --git a/server/src/functions/engines/recommendation.ts b/server/src/functions/engines/recommendation.ts index ffcc620c..64222a0a 100644 --- a/server/src/functions/engines/recommendation.ts +++ b/server/src/functions/engines/recommendation.ts @@ -1,29 +1,37 @@ -import { recommend as sql } from "@prisma/client/sql"; -import { Err, Ok, type Result } from "../../common/lib/result"; -import type { User, UserID } from "../../common/types"; +import { recommend } from "@prisma/client/sql"; +import type { UserID, UserWithCoursesAndSubjects } from "common/types"; import { prisma } from "../../database/client"; +import { getCoursesByUserId } from "../../database/courses"; +import * as interest from "../../database/interest"; import { getUserByID } from "../../database/users"; export async function recommendedTo( user: UserID, limit: number, offset: number, -): Promise>> { - try { - const result = await prisma.$queryRawTyped(sql(user, limit, offset)); - return Promise.all( - result.map(async (res) => { - const user = await getUserByID(res.id); - if (!user.ok) throw new Error("not found"); // this shouldn't happen - return { - count: Number.parseInt(res.overlap?.toString() ?? "0"), - u: user.value, - }; - }), - ) - .then((val) => Ok(val)) - .catch((err) => Err(err)); - } catch (err) { - return Err(err); - } +): Promise< + Array<{ + u: UserWithCoursesAndSubjects; + count: number; + }> +> { + const result = await prisma.$queryRawTyped(recommend(user, limit, offset)); + return Promise.all( + result.map(async (res) => { + const { overlap: count, ...u } = res; + if (count === null) throw new Error("count is null: something is wrong"); + // TODO: user の情報はここで再度 DB に問い合わせるのではなく、 recommend の sql で取得 + const user = await getUserByID(u.id); + const courses = getCoursesByUserId(u.id); + const subjects = interest.of(u.id); + return { + count: Number(count), + u: { + ...user, + courses: await courses, + interestSubjects: await subjects, + }, + }; + }), + ); } diff --git a/server/src/functions/img/compress.ts b/server/src/functions/img/compress.ts index 38ae559b..e4947158 100644 --- a/server/src/functions/img/compress.ts +++ b/server/src/functions/img/compress.ts @@ -1,17 +1,7 @@ import sharp from "sharp"; -import { Err, Ok, type Result } from "../../common/lib/result"; const IMAGE_SIZE_PX = 320; -export async function compressImage(buf: Buffer): Promise> { - try { - return sharp(buf) - .resize(IMAGE_SIZE_PX) - .webp() - .toBuffer() - .then((buf) => Ok(buf)) - .catch((e) => Err(e)); - } catch (e) { - return Err(e); - } +export async function compressImage(buf: Buffer): Promise { + return sharp(buf).resize(IMAGE_SIZE_PX).webp().toBuffer(); } diff --git a/server/src/functions/share/http.ts b/server/src/functions/share/http.ts index db7f811c..dd186952 100644 --- a/server/src/functions/share/http.ts +++ b/server/src/functions/share/http.ts @@ -1,11 +1,13 @@ +import type { StatusCode } from "hono/utils/http-status"; + export type Response = | { - code: number; + code: StatusCode; ok: true; body: T; } | { - code: number; + code: StatusCode; ok: false; body: string; }; diff --git a/server/src/functions/user.test.ts b/server/src/functions/user.test.ts index 36c316f4..ba04f302 100644 --- a/server/src/functions/user.test.ts +++ b/server/src/functions/user.test.ts @@ -16,7 +16,8 @@ test("get all users", async () => { expect(result.code).toBe(200); expect(result.body).toSatisfy((s) => s.length === 3); expect(result.body).toSatisfy( - (s) => typeof s !== "string" && s[0].name === "田中太郎", + (s) => + typeof s !== "string" && s.some((person) => person.name === "田中太郎"), ); }); diff --git a/server/src/functions/user.ts b/server/src/functions/user.ts index d89f7b65..fd641e30 100644 --- a/server/src/functions/user.ts +++ b/server/src/functions/user.ts @@ -1,50 +1,44 @@ -import { Result } from "../common/lib/result"; -import type { GUID, User, UserID } from "../common/types"; +import type { + GUID, + User, + UserID, + UserWithCoursesAndSubjects, +} from "common/types"; import { getMatchedUser } from "../database/requests"; import * as db from "../database/users"; import * as http from "./share/http"; export async function getAllUsers(): Promise> { const users = await db.getAllUsers(); - if (!users.ok) { - console.error(users.error); - return http.internalError(); - } - return http.ok(users.value); + return http.ok(users); } -export async function getUser(guid: GUID): Promise> { +export async function getUser( + guid: GUID, +): Promise> { const user = await db.getUser(guid); - if (!user.ok) { - if (user.error === 404) return http.notFound(); - console.error(user.error); - return http.internalError(); - } - return http.ok(user.value); + return http.ok(user); } export async function getUserByID( userId: UserID, ): Promise> { const user = await db.getUserByID(userId); - if (!user.ok) { - if (user.error === 404) return http.notFound(); - console.error(user.error); - return http.internalError(); - } - return http.ok(user.value); + return http.ok(user); } export async function userExists(guid: GUID): Promise> { const user = await db.getUser(guid); - if (user.ok) return http.ok(undefined); - if (user.error === 404) return http.notFound(undefined); + if (user) return http.ok(undefined); + if (user === 404) return http.notFound(undefined); return http.internalError("db error"); } -export async function getMatched(user: UserID): Promise> { +export async function getMatched( + user: UserID, +): Promise> { const matchedUsers = await getMatchedUser(user); - if (!matchedUsers.ok) return http.internalError(); + if (!matchedUsers) return http.internalError(); - return http.ok(matchedUsers.value); + return http.ok(matchedUsers); } diff --git a/server/src/index.ts b/server/src/index.ts index de19b304..7243dc3f 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,58 +1,55 @@ -import cookieParser from "cookie-parser"; -import express from "express"; -import csrf from "./lib/cross-origin/block-unknown-origin"; -import cors from "./lib/cross-origin/multi-origin-cors"; -import { initializeSocket } from "./lib/socket/socket"; +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { HTTPException } from "hono/http-exception"; +import { allUrlMustBeValid, env } from "./lib/utils"; import chatRoutes from "./router/chat"; import coursesRoutes from "./router/courses"; import matchesRoutes from "./router/matches"; import pictureRoutes from "./router/picture"; import requestsRoutes from "./router/requests"; +import sseRoutes from "./router/sse"; +import subjectsRoutes from "./router/subjects"; import usersRoutes from "./router/users"; -const app = express(); +const allowedOrigins = env("CORS_ALLOW_ORIGINS") + .split(",") + .filter((s) => s); +allUrlMustBeValid(allowedOrigins); -// 高度なクエリパーサーを使わないよう設定。これによりクエリパラメータが配列やオブジェクトではなく string になるようにしている。 -// https://expressjs.com/ja/api.html#app.settings.table の query parser を参照。 -app.set("query parser", "simple"); - -const port = 3000; -const allowedOrigins = [ - process.env.SERVER_ORIGIN ?? "http://localhost:3000", // delete this fallback when you think everyone has updated their .env - process.env.WEB_ORIGIN, - process.env.MOBILE_ORIGIN, - process.env.WEB_ORIGIN_BUILD, -]; -export const corsOptions = { - origins: allowedOrigins.filter((s) => s != null).filter((s) => s), // ignore empty string too - methods: ["GET", "HEAD", "POST", "PUT", "DELETE"], +const corsOptions = { + origin: allowedOrigins, credentials: true, }; -app.use(cors(corsOptions)); -app.use(csrf(corsOptions)); - -app.use(express.json()); -app.use(express.urlencoded({ extended: true })); -// app.use(cookieParser()); - -app.get("/", (_, res) => { - res.json("Hello from Express!"); -}); - -// ルーティング -app.use("/picture", pictureRoutes); -app.use("/users", usersRoutes); -app.use("/courses", coursesRoutes); -app.use("/requests", requestsRoutes); -app.use("/matches", matchesRoutes); -app.use("/chat", chatRoutes); - -export function main() { - // サーバーの起動 - const server = app.listen(port, () => { - console.log("running"); - }); - initializeSocket(server, corsOptions); - return server; +if (corsOptions.origin.length > 1) { + console.warn( + "WARNING: socket.io only supports one cors origin, therefore only first origin will be registered.", + ); } + +const app = new Hono() + .onError((err, c) => { + if (err instanceof HTTPException) { + console.log(err); + return c.json({ error: err.message }, err.status); + } + console.error(err); + return c.json({ error: err }, 500); + }) + + .use(cors(corsOptions)) + + .get("/", async (c) => { + return c.text("Hello from Hono 🔥"); + }) + // ルーティング + .route("/picture", pictureRoutes) + .route("/users", usersRoutes) + .route("/courses", coursesRoutes) + .route("/subjects", subjectsRoutes) + .route("/requests", requestsRoutes) + .route("/matches", matchesRoutes) + .route("/chat", chatRoutes) + .route("/sse", sseRoutes); + +export default app; diff --git a/server/src/lib/cross-origin/block-unknown-origin.ts b/server/src/lib/cross-origin/block-unknown-origin.ts deleted file mode 100644 index 9961a77f..00000000 --- a/server/src/lib/cross-origin/block-unknown-origin.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Request, Response } from "express"; -import { type Config, validateConfig } from "./share"; - -function serverSideBlocking(config: Config) { - validateConfig(config); - return (req: Request, res: Response, next: () => void) => { - const reqOrigin = req.header("Origin"); - if (!reqOrigin) { - // no origin header == no cors == same origin - next(); - return; - } - let ok = false; - if (config.origins.includes(reqOrigin)) ok = true; - if (config.origins[0] === "*") ok = true; - if (ok) { - // ok: known origin or allowing all origins - next(); - return; - } - res.status(403).send(`unknown origin header: ${reqOrigin}`); - }; -} - -export default serverSideBlocking; diff --git a/server/src/lib/cross-origin/multi-origin-cors.ts b/server/src/lib/cross-origin/multi-origin-cors.ts deleted file mode 100644 index f51d1880..00000000 --- a/server/src/lib/cross-origin/multi-origin-cors.ts +++ /dev/null @@ -1,48 +0,0 @@ -import cors from "cors"; -import { type Config, validateConfig } from "./share"; - -/* expected use case: - -const origins = [ "localhost:3000", "localhost:5173" ]; -const corsConfig = { origins }; -const app = express(); - -app.use(csrf(corsConfig)); - -*/ - -// express middlewares. -// ref: https://expressjs.com/ja/guide/using-middleware.html - -// CORS is a method to bypass client-side SOP. -// NOTE: even if CORS is enabled, the server is still vulnerable to CSRF attacks. -// use the serverSideBlocking() below for CSRF. -// more about CORS: -// - https://developer.mozilla.org/ja/docs/Web/HTTP/CORS - -export default (config: Config) => { - validateConfig(config); - - function origin( - origin: string | undefined, - callback: (error: Error | null, flag?: boolean) => void, - ) { - // origin not found === same origin request (or non-browser request) - if (!origin) { - return callback(null, true); - } - - // origin in allowedOrigins. good. - if (config.origins.includes(origin)) { - return callback(null, true); - } - - // origin exists and not in allowedOrigins. bad. - return callback(new Error("Not allowed by CORS")); - } - - return cors({ - origin: origin, - credentials: config.credentials, - }); -}; diff --git a/server/src/lib/cross-origin/share.ts b/server/src/lib/cross-origin/share.ts deleted file mode 100644 index b98e89d7..00000000 --- a/server/src/lib/cross-origin/share.ts +++ /dev/null @@ -1,50 +0,0 @@ -type Config = { - origins: string[]; // allowed origins - methods?: string[]; // Access-Control-Allow-Methods - credentials?: boolean; // Access-Control-Allow-Credentials -}; - -// make the cors config valid. -// throws error if unrecoverable. -function validateConfig(config: Config) { - config.credentials = !!config.credentials; // make it boolean. not using Boolean() or new Boolean() because I don't trust JS - - // normalize allowOrigin URLs - config.origins = config.origins.map((origin) => { - const url = new URL(origin); - if (url.origin === "null") { - console.log( - `invalid URL: ${origin}. Please prefix this with http:// or https:// if you haven't.`, - ); - throw ""; - } - return url.origin; - }); - - if (!config.methods) config.methods = []; // provide default - - // they must be uppercase - config.methods = config.methods.map((method) => method.toUpperCase()); - - // GET and HEAD must be in this field. POST is for convenience. - // ref: not found - const defaultAllowedMethods = ["GET", "HEAD", "POST"]; - for (const defaultMethod of defaultAllowedMethods) { - if (!config.methods.includes(defaultMethod)) { - config.methods.push(defaultMethod); - } - } - - assertValidConfig(config); -} - -// this throws error if config is not good -function assertValidConfig(config: Config) { - if (config.origins.length === 0) { - throw new Error( - `Empty allowedOrigins in CORS config: ${JSON.stringify(config)}`, - ); - } -} - -export { validateConfig, type Config }; diff --git a/server/src/lib/error.ts b/server/src/lib/error.ts new file mode 100644 index 00000000..39132428 --- /dev/null +++ b/server/src/lib/error.ts @@ -0,0 +1,7 @@ +import { HTTPException } from "hono/http-exception"; +import type { ContentfulStatusCode } from "hono/utils/http-status"; + +// expected error +export function error(reason: string, code: ContentfulStatusCode): never { + throw new HTTPException(code, { message: reason }); +} diff --git a/server/src/lib/hash.ts b/server/src/lib/hash.ts new file mode 100644 index 00000000..e4a6cc3e --- /dev/null +++ b/server/src/lib/hash.ts @@ -0,0 +1,7 @@ +import crypto from "node:crypto"; + +export function sha256(src: string): string { + const hasher = crypto.createHash("sha256"); + hasher.update(src); + return hasher.digest("hex"); +} diff --git a/server/src/lib/socket/socket.ts b/server/src/lib/socket/socket.ts deleted file mode 100644 index 84124a55..00000000 --- a/server/src/lib/socket/socket.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { Server } from "node:http"; -import type { CorsOptions } from "cors"; -import { type Socket, Server as SocketIOServer } from "socket.io"; -import type { Message, UserID } from "../../common/types"; -import { getUserIdFromToken } from "../../firebase/auth/db"; - -const users = new Map(); - -export function initializeSocket(server: Server, corsOptions: CorsOptions) { - const io = new SocketIOServer(server, { - cors: corsOptions, - connectionStateRecovery: {}, - }); - - io.on("connection", (socket) => { - socket.on("register", async (token) => { - const userId = await getUserIdFromToken(token); - if (userId) { - users.set(userId, socket); - } else { - console.log("Invalid token or failed to retrieve user ID"); - } - }); - - socket.on("disconnect", () => { - for (const [id, socket2] of users.entries()) { - if (socket2.id === socket.id) { - users.delete(id); - break; - } - } - }); - }); -} - -export function sendMessage(message: Message, friendId: UserID) { - const socket = users.get(friendId); - if (socket) { - socket.emit("newMessage", message); - } -} - -export function updateMessage(message: Message, friendId: UserID) { - const socket = users.get(friendId); - if (socket) { - socket.emit("updateMessage", message); - } -} - -export function deleteMessage(messageId: number, friendId: UserID) { - const socket = users.get(friendId); - if (socket) { - socket.emit("deleteMessage", messageId); - } -} diff --git a/server/src/lib/utils.ts b/server/src/lib/utils.ts new file mode 100644 index 00000000..229aa65b --- /dev/null +++ b/server/src/lib/utils.ts @@ -0,0 +1,16 @@ +import { panic } from "common/lib/panic"; + +export function allUrlMustBeValid(urls: string[]) { + for (const url of urls) { + try { + new URL(url); + } catch (err) { + console.error(err); + throw err; + } + } +} + +export function env(name: string) { + return process.env[name] || panic(`env ${name} is missing`); +} diff --git a/server/src/lib/validator.ts b/server/src/lib/validator.ts new file mode 100644 index 00000000..bcaae535 --- /dev/null +++ b/server/src/lib/validator.ts @@ -0,0 +1,18 @@ +import { zValidator } from "@hono/zod-validator"; +import { type Schema, z } from "zod"; + +export function json(schema: T) { + return zValidator("json", schema); +} + +export function param>(schema: T) { + return zValidator("param", z.object(schema)); +} +export function query>(schema: T) { + return zValidator("query", z.object(schema)); +} + +export default { + json, + param, +}; diff --git a/server/src/main.ts b/server/src/main.ts deleted file mode 100644 index efc922b6..00000000 --- a/server/src/main.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { main } from "./index"; - -main(); diff --git a/server/src/router/chat.ts b/server/src/router/chat.ts index 01369a4c..6d0bb5ce 100644 --- a/server/src/router/chat.ts +++ b/server/src/router/chat.ts @@ -1,148 +1,169 @@ -import express from "express"; -import { safeParseInt } from "../common/lib/result/safeParseInt"; -import type { MessageID, UserID } from "../common/types"; -import { parseUserID } from "../common/zod/methods"; +import type { MessageID } from "common/types"; +import { parseUserID } from "common/zod/methods"; import { ContentSchema, InitRoomSchema, SendMessageSchema, SharedRoomSchema, -} from "../common/zod/schemas"; +} from "common/zod/schemas"; +import { Hono } from "hono"; +import { HTTPException } from "hono/http-exception"; +import { z } from "zod"; import * as db from "../database/chat"; -import { safeGetUserId } from "../firebase/auth/db"; +import { prisma } from "../database/client"; +import { getUserId } from "../firebase/auth/db"; import * as core from "../functions/chat"; -import * as ws from "../lib/socket/socket"; - -const router = express.Router(); - -router.get("/overview", async (req, res) => { - const id = await safeGetUserId(req); - if (!id.ok) return res.status(401).send("auth error"); - - const result = await core.getOverview(id.value); - res.status(result.code).send(result.body); -}); - -// send DM to userid. -router.post("/dm/to/:userid", async (req, res) => { - const user = await safeGetUserId(req); - if (!user.ok) return res.status(401).send("auth error"); - const friend = safeParseInt(req.params.userid); - if (!friend.ok) return res.status(400).send("bad param encoding: `userid`"); - - const send = SendMessageSchema.safeParse(req.body); - if (!send.success) { - return res.status(400).send("invalid format"); - } - - const result = await core.sendDM(user.value, friend.value, send.data); - if (result.ok) { - ws.sendMessage(result?.body, friend.value); - } - res.status(result.code).send(result.body); -}); - -// GET a DM Room with userid, CREATE one if not found. -router.get("/dm/with/:userid", async (req, res) => { - const user = await safeGetUserId(req); - if (!user.ok) return res.status(401).send("auth error"); - - const friend = safeParseInt(req.params.userid); - if (!friend.ok) - return res.status(400).send("invalid param `userid` formatting"); - - const result = await core.getDM(user.value, friend.value); - - return res.status(result.code).send(result.body); -}); - -// create a shared chat room. -router.post("/shared", async (req, res) => { - const user = await safeGetUserId(req); - if (!user.ok) return res.status(401).send("auth error"); - - const init = InitRoomSchema.safeParse(req.body); - if (!init.success) return res.status(400).send("invalid format"); - - const result = await core.createRoom(user.value, init.data); - - return res.status(result.code).send(result.body); -}); - -router.get("/shared/:roomId", async (req, res) => { - const user = await safeGetUserId(req); - if (!user.ok) return res.status(401).send("auth error"); - const roomId = safeParseInt(req.params.roomId); - if (!roomId.ok) return res.status(400).send("invalid formatting of :roomId"); - - const result = await core.getRoom(user.value, roomId.value); - return res.status(result.code).send(result.body); -}); - -/** - * PATCH -> update room info. (except the message log). - * - body: UpdateRoom - **/ -router.patch("/shared/:room", async (req, res) => { - const user = await safeGetUserId(req); - if (!user.ok) return res.status(401).send("auth error"); - const roomId = safeParseInt(req.params.room); - if (!roomId.ok) return res.status(400).send("invalid :room"); - - const room = SharedRoomSchema.safeParse(req.body); - if (!room.success) return res.status(400).send("invalid format"); - - // todo: type check - const result = await core.patchRoom(user.value, roomId.value, room.data); - res.status(result.code).send(result.body); -}); - -// POST: authorized body=UserID[] -router.post("/shared/id/:room/invite", async (req, res) => { - const user = await safeGetUserId(req); - if (!user.ok) return res.status(401).send("auth error"); - const roomId = safeParseInt(req.params.room); - if (!roomId.ok) return res.status(400).send("invalid :room"); - - const invited: UserID[] = req.body; - try { - if (!Array.isArray(invited)) throw new TypeError(); +import { json, param } from "../lib/validator"; +import * as sse from "../router/sse"; + +const userid_param = param({ userid: z.coerce.number() }); +const router = new Hono() + .get("/overview", async (c) => { + const id = await getUserId(c); + const result = await core.getOverview(id); + return c.json(result.body); // status: result.code + }) + + // send DM to userId. + .post("/dm/to/:userid", userid_param, json(SendMessageSchema), async (c) => { + const user = await getUserId(c); + const friend = c.req.valid("param").userid; + const send = c.req.valid("json"); + const result = await core.sendDM(user, friend, send); + const senderName = await prisma.user.findUnique({ + where: { + id: user, + }, + select: { + name: true, + }, + }); + if (!senderName) + throw new HTTPException(500, { message: "sender not found???" }); + + sse.send(friend, { + event: "Chat:Append", + data: { message: result, sender: senderName?.name }, + }); + c.status(201); + return c.json(result); + }) + + // GET a DM Room with userId, CREATE one if not found. + .get("/dm/with/:userid", userid_param, async (c) => { + const user = await getUserId(c); + const friend = c.req.valid("param").userid; + const result = await core.getDM(user, friend); + return c.json(result.body); // status: result.code + }) + + .post( + "/mark-as-read/:rel/:messageId", + param({ messageId: z.coerce.number(), rel: z.coerce.number() }), + async (c) => { + const user = await getUserId(c); + const { messageId, rel } = c.req.valid("param"); + await db.markAsRead(rel, user, messageId); + return c.text("ok"); + }, + ) + + // create a shared chat room. + .post("/shared", json(InitRoomSchema), async (c) => { + const user = await getUserId(c); + const init = c.req.valid("json"); + const result = await core.createRoom(user, init); + c.status(result.code); + return c.json(result.body); + }) + + .get("/shared/:roomId", param({ roomId: z.coerce.number() }), async (c) => { + const user = await getUserId(c); + const roomId = c.req.valid("param").roomId; + const result = await core.getRoom(user, roomId); + return c.json(result.body); + }) + + /** + * PATCH -> update room info. (except the message log). + * - body: UpdateRoom + **/ + .patch( + "/shared/:room", + param({ room: z.coerce.number() }), + json(SharedRoomSchema), + async (c) => { + const user = await getUserId(c); + const roomId = c.req.valid("param").room; + const room = c.req.valid("json"); + const result = await core.patchRoom(user, roomId, room); + c.status(result.code); + return c.json(result.body); + }, + ) + + // POST: authorized body=UserID[] + .post("/shared/id/:room/invite", param({ room: z.number() }), async (c) => { + const user = await getUserId(c); + const roomId = c.req.valid("param").room; + + const invited = z.array(z.number()).parse(c.body); invited.map(parseUserID); - } catch (_) { - return res.status(400).send("bad formatting"); - } - - const result = await core.inviteUserToRoom(user.value, invited, roomId.value); - return res.status(result.code).send(result.body); -}); - -router.patch("/messages/id/:id", async (req, res) => { - const user = await safeGetUserId(req); - if (!user.ok) return res.status(401).send("auth error"); - const id = safeParseInt(req.params.id); - if (!id.ok) return res.status(400).send("invalid :id"); - const friend = req.body.friend; - - const content = ContentSchema.safeParse(req.body.newMessage.content); - if (!content.success) return res.status(400).send(); - - const result = await core.updateMessage(user.value, id.value, content.data); - res.status(result.code).send(result.body); - if (result.ok) { - ws.updateMessage(result.body, friend); - } -}); - -router.delete("/messages/id/:id", async (req, res) => { - const user = await safeGetUserId(req); - if (!user.ok) return res.status(401).send("auth error"); - const id = safeParseInt(req.params.id); - if (!id.ok) return res.status(400).send("bad `id` format"); - const friend = req.body.friend; - - await db.deleteMessage(id.value as MessageID, user.value); - ws.deleteMessage(id.value, friend); - return res.status(204).send(); -}); + + const result = await core.inviteUserToRoom(user, invited, roomId); + c.status(result.code); + return c.json(result.body); + }) + + .patch( + "/messages/id/:id", + param({ id: z.coerce.number() }), + json( + z.object({ + friend: z.number(), + newMessage: z.object({ content: ContentSchema }), + }), + ), + async (c) => { + const user = await getUserId(c); + const id = c.req.valid("param").id; + const friend = c.req.valid("json").friend; + + const content = c.req.valid("json").newMessage.content; + + const result = await core.updateMessage(user, id, content); + if (result.ok) { + sse.send(friend, { + event: "Chat:Update", + data: { + id: result.body.id, + message: result.body, + }, + }); + } + c.status(result.code); + return c.json(result.body); + }, + ) + + .delete( + "/messages/id/:id", + param({ id: z.coerce.number() }), + json(z.object({ friend: z.number() })), + async (c) => { + const user = await getUserId(c); + const id = c.req.valid("param").id; + const friend = c.req.valid("json").friend; + await db.deleteMessage(id as MessageID, user); + sse.send(friend, { + event: "Chat:Delete", + data: { + id, + }, + }); + c.status(204); + return c.json({}); + }, + ); export default router; diff --git a/server/src/router/courses.ts b/server/src/router/courses.ts index 959ae4f3..2b8e66b7 100644 --- a/server/src/router/courses.ts +++ b/server/src/router/courses.ts @@ -1,6 +1,6 @@ -import express, { type Request, type Response } from "express"; -import type { Day } from "../common/types"; -import { DaySchema, PeriodSchema } from "../common/zod/schemas"; +import { DaySchema, PeriodSchema } from "common/zod/schemas"; +import { Hono } from "hono"; +import { z } from "zod"; import { getCourseByCourseId, getCourseByDayPeriodAndUserId, @@ -8,147 +8,84 @@ import { getCoursesByUserId, } from "../database/courses"; import { createEnrollment, deleteEnrollment } from "../database/enrollments"; -import { safeGetUserId } from "../firebase/auth/db"; +import { getUserId } from "../firebase/auth/db"; +import { json, param, query } from "../lib/validator"; -const router = express.Router(); +const router = new Hono() -function isDay(value: string): value is Day { - return DaySchema.safeParse(value).success; -} + // ある曜限に存在する全ての講義を取得 + .get( + "/day-period", + query({ day: DaySchema, period: PeriodSchema }), + async (c) => { + const { day, period } = c.req.valid("query"); -// ある曜限に存在する全ての講義を取得 -router.get("/day-period", async (req: Request, res: Response) => { - const day = DaySchema.safeParse(req.query.day); - // TODO: as の使用をやめ、Request 型を適切に拡張する https://stackoverflow.com/questions/63538665/how-to-type-request-query-in-express-using-typescript - const period = PeriodSchema.safeParse( - Number.parseInt(req.query.period as string), - ); + const courses = await getCoursesByDayAndPeriod(day, period); + c.status(200); + return c.json(courses); + }, + ) - if (!day.success || !period.success || !isDay(day.data)) { - return res.status(400).json({ error: "Invalid day" }); - } - - try { - const courses = await getCoursesByDayAndPeriod(day.data, period.data); - res.status(200).json(courses); - } catch (error) { - console.error("Error fetching courses by day and period:", error); - res - .status(500) - .json({ error: "Failed to fetch courses by day and period" }); - } -}); - -// 特定のユーザが履修している講義を取得 -router.get("/userId/:userId", async (req: Request, res: Response) => { - const userId = Number.parseInt(req.params.userId); - if (Number.isNaN(userId)) { - return res.status(400).json({ error: "Invalid userId" }); - } - - try { + // 特定のユーザが履修している講義を取得 + .get("/userId/:userId", param({ userId: z.coerce.number() }), async (c) => { + const userId = c.req.valid("param").userId; const courses = await getCoursesByUserId(userId); - res.status(200).json(courses); - } catch (error) { - console.error("Error fetching courses by userId:", error); - res.status(500).json({ error: "Failed to fetch courses by userId" }); - } -}); + c.status(200); + return c.json(courses); + }) -// 自分が履修している講義を取得 -router.get("/mine", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send("auth error"); - - try { - const courses = await getCoursesByUserId(userId.value); - return res.status(200).json(courses); - } catch (error) { - console.error("Error fetching courses:", error); - res.status(500).json({ error: "Failed to fetch courses" }); - } -}); - -// ある講義と重複している自分の講義を取得 -router.get("/mine/overlaps/:courseId", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send("auth error"); + // 自分が履修している講義を取得 + .get("/mine", async (c) => { + const userId = await getUserId(c); + const courses = await getCoursesByUserId(userId); + c.status(200); + return c.json(courses); + }) - try { - const targetCourse = await getCourseByCourseId(req.params.courseId); - if (!targetCourse) { - return res.status(404).json({ error: "Course not found" }); - } - const overlappingCourses = await Promise.all( - targetCourse.slots.map( - async (slot) => - await getCourseByDayPeriodAndUserId( - slot.day, - slot.period, - userId.value, - ), - ), - ); - const filteredOverlappingCourses = overlappingCourses.filter( - (course) => course !== null, - ); - const uniqueFilteredOverlappingCourses = filteredOverlappingCourses.filter( - (course, index, self) => - self.findIndex((c) => c?.id === course?.id) === index, - ); // id の重複を排除 - res.status(200).json(uniqueFilteredOverlappingCourses); - } catch (error) { - console.error("Error fetching overlapping courses:", error); - res.status(500).json({ error: "Failed to fetch overlapping courses" }); - } -}); + // ある講義と重複している自分の講義を取得 + .get( + "/mine/overlaps/:courseId", + param({ courseId: z.string() }), + async (c) => { + const userId = await getUserId(c); + const targetCourse = await getCourseByCourseId( + c.req.valid("param").courseId, + ); + const overlappingCourses = await Promise.all( + targetCourse.slots.map( + async (slot) => + await getCourseByDayPeriodAndUserId(slot.day, slot.period, userId), + ), + ); + const filteredOverlappingCourses = overlappingCourses.filter( + (course) => course !== null, + ); + const uniqueFilteredOverlappingCourses = + filteredOverlappingCourses.filter( + (course, index, self) => + self.findIndex((c) => c?.id === course?.id) === index, + ); // id の重複を排除 + c.status(200); + return c.json(uniqueFilteredOverlappingCourses); + }, + ) -// 自分の講義を編集 -router.patch("/mine", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send("auth error"); - const { courseId } = req.body; - // 指定された講義の存在確認 - try { - const newCourse = await getCourseByCourseId(courseId); - if (!newCourse) { - return res.status(404).json({ error: "Course not found" }); - } - } catch (err) { - console.error("Error fetching course:", err); - res.status(500).json({ error: "Failed to fetch course" }); - } - try { - const updatedCourses = await createEnrollment(courseId, userId.value); - res.status(200).json(updatedCourses); - } catch (error) { - console.error("Error updating courses:", error); - res.status(500).json({ error: "Failed to update courses" }); - } -}); + // 自分の講義を編集 + .patch("/mine", json(z.object({ courseId: z.string() })), async (c) => { + const userId = await getUserId(c); + const { courseId } = c.req.valid("json"); + const updatedCourses = await createEnrollment(courseId, userId); + c.status(200); + return c.json(updatedCourses); + }) -// 自分の講義を削除 -router.delete("/mine", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send("auth error"); - const { courseId } = req.body; - // 指定された講義の存在確認 - try { - const newCourse = await getCourseByCourseId(courseId); - if (!newCourse) { - return res.status(404).json({ error: "Course not found" }); - } - } catch (err) { - console.error("Error fetching course:", err); - res.status(500).json({ error: "Failed to fetch course" }); - } - try { - const updatedCourses = await deleteEnrollment(userId.value, courseId); - res.status(200).json(updatedCourses); - } catch (error) { - console.error("Error deleting courses:", error); - res.status(500).json({ error: "Failed to delete courses" }); - } -}); + // 自分の講義を削除 + .delete("/mine", json(z.object({ courseId: z.string() })), async (c) => { + const userId = await getUserId(c); + const { courseId } = c.req.valid("json"); + const updatedCourses = await deleteEnrollment(userId, courseId); + c.status(200); + return c.json(updatedCourses); + }); export default router; diff --git a/server/src/router/matches.ts b/server/src/router/matches.ts index 6282be64..859722f9 100644 --- a/server/src/router/matches.ts +++ b/server/src/router/matches.ts @@ -1,43 +1,35 @@ -import express, { type Request, type Response } from "express"; -import { safeParseInt } from "../common/lib/result/safeParseInt"; -import type { UserID } from "../common/types"; +import type { UserID } from "common/types"; +import { Hono } from "hono"; +import { z } from "zod"; import { deleteMatch, getMatchesByUserId } from "../database/matches"; -import { safeGetUserId } from "../firebase/auth/db"; +import { getUserId } from "../firebase/auth/db"; +import { param } from "../lib/validator"; -const router = express.Router(); +const router = new Hono() -// SELECT * FROM "Relationship" WHERE user in (.sender, .recv) AND status = MATCHED -router.get("/", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send("auth error"); + // SELECT * FROM "Relationship" WHERE user in (.sender, .recv) AND status = MATCHED + .get("/", async (c) => { + const userId = await getUserId(c); + const all = await getMatchesByUserId(userId); + const matched = all.filter((relation) => relation.status === "MATCHED"); + return c.json(matched); + }) - try { - const all = await getMatchesByUserId(userId.value); - if (!all.ok) return res.status(500).send(); - const matched = all.value.filter( - (relation) => relation.status === "MATCHED", - ); - res.status(200).json(matched); - } catch (error) { - console.error("Error fetching matches:", error); - res.status(500).json({ error: "Failed to fetch matches" }); - } -}); + // フレンドの削除 + .delete( + "/:opponentId", + param({ + opponentId: z.coerce.number(), + }), + async (c) => { + const opponentId = c.req.valid("param").opponentId; + // 削除操作を要求しているユーザ + const requesterId = await getUserId(c); -// フレンドの削除 -router.delete("/:opponentId", async (req: Request, res: Response) => { - const opponentId = safeParseInt(req.params.opponentId); - if (!opponentId.ok) return res.status(400).send("bad param encoding"); - - // 削除操作を要求しているユーザ - const requesterId = await safeGetUserId(req); - if (!requesterId.ok) return res.status(401).send("auth error"); - - const result = await deleteMatch( - requesterId.value, - opponentId.value as UserID, + await deleteMatch(requesterId, opponentId as UserID); + c.status(204); + return c.text(""); + }, ); - res.status(result.ok ? 204 : 500).send(); -}); export default router; diff --git a/server/src/router/picture.ts b/server/src/router/picture.ts index 651e2bdf..18fc8361 100644 --- a/server/src/router/picture.ts +++ b/server/src/router/picture.ts @@ -1,41 +1,84 @@ -import bodyParser from "body-parser"; -import express from "express"; +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { bodyLimit } from "hono/body-limit"; +import { z } from "zod"; +import * as chat from "../database/chat"; +import * as relation from "../database/matches"; import * as storage from "../database/picture"; -import { safeGetGUID } from "../firebase/auth/lib"; +import { getUserId } from "../firebase/auth/db"; +import { getGUID } from "../firebase/auth/lib"; import { compressImage } from "../functions/img/compress"; +import { error } from "../lib/error"; +import * as hashing from "../lib/hash"; -// TODO: truncate file at frontend s.t. even the largest file won't trigger the limit -const parseLargeBuffer = bodyParser.raw({ - type: "image/png", - // TODO: block large files (larger than 1mb? idk) - limit: "5mb", -}); -const router = express.Router(); - -router.get("/:guid", async (req, res) => { - const guid = req.params.guid; - const result = await storage.get(guid); - switch (result.ok) { - case true: - return res.send(result.value); - case false: - return res.status(404).send(); - } +const largeLimit = bodyLimit({ + maxSize: 50 * 1024 * 1024, // 50mb + onError: (c) => { + return c.text("overflow :(", 413); + }, }); +const router = new Hono() -router.post("/", parseLargeBuffer, async (req, res) => { - const guid = await safeGetGUID(req); - if (!guid.ok) return res.status(401).send(); + /* General Pictures in chat */ - if (!Buffer.isBuffer(req.body)) return res.status(404).send("not buffer"); + .post( + "/to/:userId", + zValidator("param", z.object({ userId: z.coerce.number() })), + largeLimit, + async (c) => { + const sender = await getUserId(c); + const recv = c.req.valid("param").userId; - const buf = await compressImage(req.body); - if (!buf.ok) return res.status(500).send("failed to compress image"); + const rel = await relation.getRelation(sender, recv); + if (rel.status !== "MATCHED") error("not matched", 401); - const url = await storage.set(guid.value, buf.value); - if (!url.ok) return res.status(500).send("failed to upload image"); + const buf = new Buffer(await c.req.arrayBuffer()); + const hash = hashing.sha256(buf.toString("base64")); + const passkey = hashing.sha256(crypto.randomUUID()); - return res.status(201).type("text/plain").send(url.value); -}); + return storage.uploadPic(hash, buf, passkey).then(async (url) => { + await chat.createImageMessage(sender, rel.id, url); + return c.text(url); + }); + }, + ) + + .get( + "/:id", + zValidator("param", z.object({ id: z.string() })), + zValidator("query", z.object({ key: z.string() })), + async (c) => { + const hash = c.req.valid("param").id; + const key = c.req.valid("query").key; + if (!key) error("key is required", 400); + + return storage.getPic(hash, String(key)).then((buf) => { + if (buf) { + return c.body(buf); + } + }); + }, + ) + + /* Profile Pictures */ + + .get( + "/profile/:guid", + zValidator("param", z.object({ guid: z.string() })), + async (c) => { + const guid = c.req.valid("param").guid; + const result = await storage.getProf(guid); + return c.body(result); + }, + ) + + .post("/profile", largeLimit, async (c) => { + const guid = await getGUID(c); + const buf = await compressImage(new Buffer(await c.req.arrayBuffer())); + const url = await storage.setProf(guid, buf); + + c.status(201); + return c.text(url); + }); export default router; diff --git a/server/src/router/requests.ts b/server/src/router/requests.ts index a4bf11e2..34e8fd6b 100644 --- a/server/src/router/requests.ts +++ b/server/src/router/requests.ts @@ -1,87 +1,72 @@ -import express, { type Request, type Response } from "express"; -import type { UserID } from "../common/types"; - -import { safeParseInt } from "../common/lib/result/safeParseInt"; +import { zValidator } from "@hono/zod-validator"; +import type { UserID } from "common/types"; +import { Hono } from "hono"; +import { z } from "zod"; import { approveRequest, cancelRequest, rejectRequest, sendRequest, } from "../database/requests"; -import { safeGetUserId } from "../firebase/auth/db"; -// import { Relationship } from "@prisma/client"; // ... not used? - -const router = express.Router(); - -// リクエストの送信 -router.put("/send/:receiverId", async (req: Request, res: Response) => { - const receiverId = safeParseInt(req.params.receiverId); - if (!receiverId.ok) return res.status(400).send("bad param encoding"); - - const senderId = await safeGetUserId(req); - if (!senderId.ok) return res.status(401).send("auth error"); - - try { - const sentRequest = await sendRequest({ - senderId: senderId.value, - receiverId: receiverId.value as UserID, - }); - res.status(201).json(sentRequest); - } catch (error) { - console.error("Error sending match request:", error); - res.status(500).json({ error: "Failed to send match request" }); - } -}); - -// リクエストの承認 -router.put("/accept/:senderId", async (req: Request, res: Response) => { - const senderId = safeParseInt(req.params.senderId); - if (!senderId.ok) return res.status(400).send("bad param encoding"); - - const receiverId = await safeGetUserId(req); - if (!receiverId.ok) return res.status(401).send("auth error"); - - try { - await approveRequest(senderId.value as UserID, receiverId.value); - res.status(201).send(); - } catch (error) { - console.error("Error approving match request:", error); - res.status(500).json({ error: "Failed to approve match request" }); - } -}); - -router.put("/cancel/:opponentId", async (req: Request, res: Response) => { - const opponentId = safeParseInt(req.params.opponentId); - if (!opponentId.ok) return res.status(400).send("bad param encoding"); - - const requesterId = await safeGetUserId(req); - if (!requesterId.ok) return res.status(401).send("auth error"); - - const result = await cancelRequest(requesterId.value, opponentId.value); - - switch (result.ok) { - case true: - return res.status(204).send(); - case false: - return res.status(500).send(); - } -}); - -// リクエストの拒否 -router.put("/reject/:opponentId", async (req: Request, res: Response) => { - const opponentId = safeParseInt(req.params.opponentId); - if (!opponentId.ok) return res.status(400).send("bad param encoding"); - - const requesterId = await safeGetUserId(req); - if (!requesterId.ok) return res.status(401).send("auth error"); - - try { - await rejectRequest(opponentId.value as UserID, requesterId.value); //TODO 名前を良いのに変える - res.status(204).send(); - } catch (error) { - console.error("Error rejecting match request:", error); - res.status(500).json({ error: "Failed to reject match request" }); - } -}); +import { getUserId } from "../firebase/auth/db"; +import { error } from "../lib/error"; +import { param } from "../lib/validator"; + +const router = new Hono() + + // リクエストの送信 + .put( + "/send/:receiverId", + zValidator("param", z.object({ receiverId: z.coerce.number() })), + async (c) => { + const receiverId = c.req.valid("param").receiverId; + const senderId = await getUserId(c); + const sentRequest = await sendRequest({ + senderId: senderId, + receiverId: receiverId as UserID, + }); + c.status(201); + return c.json(sentRequest); + }, + ) + + // リクエストの承認 + .put( + "/accept/:senderId", + param({ senderId: z.coerce.number() }), + async (c) => { + const senderId = c.req.valid("param").senderId; + const receiverId = await getUserId(c); + + await approveRequest(senderId as UserID, receiverId); + c.status(201); + return c.json({}); + }, + ) + + .put( + "/cancel/:opponentId", + param({ opponentId: z.coerce.number() }), + async (c) => { + const opponentId = c.req.valid("param").opponentId; + const requesterId = await getUserId(c); + await cancelRequest(requesterId, opponentId); + return c.json({}); + }, + ) + + // リクエストの拒否 + .put( + "/reject/:opponentId", + param({ opponentId: z.coerce.number() }), + async (c) => { + const opponentId = c.req.valid("param").opponentId; + const requesterId = await getUserId(c); + + await rejectRequest(opponentId as UserID, requesterId); //TODO 名前を良いのに変える + c.status(204); + return c.json({}); + }, + ); export default router; diff --git a/server/src/router/sse.ts b/server/src/router/sse.ts new file mode 100644 index 00000000..82d02c11 --- /dev/null +++ b/server/src/router/sse.ts @@ -0,0 +1,50 @@ +import type { SSEChatEvent, SSEChatEventEnum, UserID } from "common/types"; +import { Hono } from "hono"; +import { HTTPException } from "hono/http-exception"; +import { streamSSE } from "hono/streaming"; +import { getUserIdFromToken } from "../firebase/auth/db"; + +export const sseChatPath = (id: UserID) => `sse:chat:${id}`; +export function send( + to: UserID, + event: SSEChatEvent, +) { + const bc = new BroadcastChannel(sseChatPath(to)); + bc.postMessage(event); + bc.close(); +} + +// https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API +// https://hono.dev/docs/helpers/streaming +const route = new Hono().get("/chat", async (c) => { + const token = c.req.query("token"); + if (!token) + throw new HTTPException(400, { + message: "token required in param", + }); + + const userId = await getUserIdFromToken(token); + return streamSSE(c, async (stream) => { + const bc = new BroadcastChannel(sseChatPath(userId)); + bc.onmessage = (e: MessageEvent) => { + const event = e.data as SSEChatEvent; + stream.writeSSE({ + event: event.event, + data: JSON.stringify(event.data), + }); + }; + + stream.onAbort(() => { + bc.close(); + }); + while (true) { + await Bun.sleep(2000); + stream.writeSSE({ + event: "Ping", + data: "", + }); + } + }); +}); + +export default route; diff --git a/server/src/router/subjects.ts b/server/src/router/subjects.ts new file mode 100644 index 00000000..17fb77b8 --- /dev/null +++ b/server/src/router/subjects.ts @@ -0,0 +1,72 @@ +import { Hono } from "hono"; +import { z } from "zod"; +import * as interest from "../database/interest"; +import { getUserId } from "../firebase/auth/db"; +import { error } from "../lib/error"; +import { json, param } from "../lib/validator"; + +const router = new Hono() + .get("/userId/:userId", param({ userId: z.coerce.number() }), async (c) => { + const userId = c.req.valid("param").userId; + const subjects = await interest.of(userId); + c.status(200); + return c.json(subjects); + }) + + .get("/mine", async (c) => { + const userId = await getUserId(c); + const subjects = await interest.of(userId); + c.status(200); + return c.json(subjects); + }) + + .post("/", json(z.object({ name: z.string() })), async (c) => { + const { name } = c.req.valid("json"); + const newSubject = await interest.create(name); + c.status(201); + return c.json(newSubject); + }) + + .patch("/mine", json(z.object({ subjectId: z.number() })), async (c) => { + const userId = await getUserId(c); + const { subjectId } = c.req.valid("json"); + const newSubject = await interest.get(subjectId); + if (!newSubject) error("subject not found", 404); + const updatedSubjects = await interest.add(userId, subjectId); + c.status(200); + return c.json(updatedSubjects); + }) + + .delete("/mine", json(z.object({ subjectId: z.number() })), async (c) => { + const userId = await getUserId(c); + const { subjectId } = c.req.valid("json"); + const updatedSubjects = await interest.remove(userId, subjectId); + c.status(200); + return c.json(updatedSubjects); + }) + + .put( + "/mine", + json(z.object({ subjectIds: z.array(z.number()) })), + async (c) => { + const userId = await getUserId(c); + const { subjectIds } = c.req.valid("json"); + const newSubjects = await Promise.all( + subjectIds.map((id) => interest.get(id)), + ); + if (newSubjects.some((s) => !s)) { + return error("Subject not found", 404); + } + await interest.updateMultipleWithTransaction(userId, subjectIds); + c.status(200); + return c.json({}); + }, + ) + + .get("/all", async (c) => { + const subjects = await interest.all(); + c.status(200); + return c.json(subjects); + }); + +export default router; diff --git a/server/src/router/users.ts b/server/src/router/users.ts index 22f823d2..1a1a6a31 100644 --- a/server/src/router/users.ts +++ b/server/src/router/users.ts @@ -1,157 +1,136 @@ -import express, { type Request, type Response } from "express"; -import type { GUID, UpdateUser } from "../common/types"; -import type { User } from "../common/types"; +import type { GUID } from "common/types"; import { GUIDSchema, InitUserSchema, UpdateUserSchema, -} from "../common/zod/schemas"; + UserIDSchema, +} from "common/zod/schemas"; +import { Hono } from "hono"; +import { z } from "zod"; +import { prisma } from "../database/client"; import { getPendingRequestsFromUser, getPendingRequestsToUser, + matchWithMemo, } from "../database/requests"; import { createUser, deleteUser, getUser, getUserByID, - unmatched, updateUser, } from "../database/users"; -import { safeGetUserId } from "../firebase/auth/db"; -import { safeGetGUID } from "../firebase/auth/lib"; +import { getUserId } from "../firebase/auth/db"; +import { getGUID } from "../firebase/auth/lib"; import { recommendedTo } from "../functions/engines/recommendation"; import * as core from "../functions/user"; - -const router = express.Router(); - -// 全ユーザーを取得 -router.get("/", async (_: Request, res: Response) => { - const result = await core.getAllUsers(); - res.status(result.code).send(result.body); -}); - -router.get("/recommended", async (req, res) => { - const u = await safeGetUserId(req); - if (!u.ok) return res.status(401).end(); - - const recommended = await recommendedTo(u.value, 20, 0); // とりあえず 20 人 - - if (recommended.ok) { - res.send(recommended.value.map((entry) => entry.u)); - } else { - res.status(500).send(recommended.error); - } -}); - -// 自分の情報を確認するエンドポイント。 -router.get("/me", async (req: Request, res: Response) => { - const guid = await safeGetGUID(req); - if (!guid.ok) return res.status(401).send("auth error"); - - const result = await core.getUser(guid.value); - res.status(result.code).send(result.body); -}); - -// ユーザーの存在を確認するためのエンドポイント。だれでもアクセス可能 -router.get("/exists/:guid", async (req: Request, res: Response) => { - const guid = req.params.guid; - const ok = await core.userExists(guid as GUID); - res.status(ok.code).send(); -}); - -// 特定のユーザーとマッチしたユーザーを取得 -router.get("/matched", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send(`auth error: ${userId.error}`); - - const result = await core.getMatched(userId.value); - res.status(result.code).json(result.body); -}); - -// ユーザーにリクエストを送っているユーザーを取得 状態はPENDING -router.get("/pending/to-me", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send(`auth error: ${userId.error}`); - - const sendingUsers = await getPendingRequestsToUser(userId.value); - if (!sendingUsers.ok) { - console.log(sendingUsers.error); - return res.status(500).send(); - } - res.status(200).json(sendingUsers.value); -}); - -// ユーザーがリクエストを送っているユーザーを取得 状態はPENDING -router.get("/pending/from-me", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send(`auth error: ${userId.error}`); - - const receivers = await getPendingRequestsFromUser(userId.value); - if (!receivers.ok) { - console.log(receivers.error); - return res.status(500).send(); - } - res.status(200).json(receivers.value); -}); - -// guidでユーザーを取得 -router.get("/guid/:guid", async (req: Request, res: Response) => { - const guid_ = GUIDSchema.safeParse(req.params.guid); - if (!guid_.success) return res.status(400).send(); - const guid = guid_.data; - - const user = await getUser(guid as GUID); - if (!user.ok) { - return res.status(404).json({ error: "User not found" }); - } - const json: User = user.value; - res.status(200).json(json); -}); - -// idでユーザーを取得 -router.get("/id/:id", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send(`auth error: ${userId.error}`); - const user = await getUserByID(userId.value); - if (!user.ok) { - return res.status(404).json({ error: "User not found" }); - } - const json: User = user.value; - res.status(200).json(json); -}); - -// INSERT INTO "User" VALUES (body...) -router.post("/", async (req: Request, res: Response) => { - const partialUser = InitUserSchema.safeParse(req.body); - if (!partialUser.success) return res.status(400).send("invalid format"); - - const user = await createUser(partialUser.data); - if (!user.ok) return res.status(500).send(); - res.status(201).json(user.value); -}); - -// ユーザーの更新エンドポイント -router.put("/me", async (req: Request, res: Response) => { - const id = await safeGetUserId(req); - if (!id.ok) return res.status(401).send("auth error"); - - const user = UpdateUserSchema.safeParse(req.body); - if (!user.success) return res.status(400).send("invalid format"); - - const updated = await updateUser(id.value, user.data); - if (!updated.ok) return res.status(500).send(); - res.status(200).json(updated.value); -}); - -// ユーザーの削除エンドポイント -router.delete("/me", async (req, res) => { - const id = await safeGetUserId(req); - if (!id.ok) return res.status(401).send("auth error"); - - const deleted = await deleteUser(id.value); - if (!deleted.ok) return res.status(500).send(); - res.status(204).send(); -}); +import { param } from "../lib/validator"; + +const router = new Hono() + + // 全ユーザーを取得 + .get("/", async (c) => { + const result = await core.getAllUsers(); + c.status(result.code); + return c.json(result.body); + }) + + .get("/recommended", async (c) => { + const u = await getUserId(c); + const recommended = await recommendedTo(u, 20, 0); // とりあえず 20 人 + return c.json(recommended.map((entry) => entry.u)); + }) + + // 自分の情報を確認するエンドポイント。 + .get("/me", async (c) => { + const guid = await getGUID(c); + const result = await core.getUser(guid); + c.status(result.code); + return c.json(result.body); + }) + + // ユーザーの存在を確認するためのエンドポイント。だれでもアクセス可能 + .get("/exists/:guid", param({ guid: z.string() }), async (c) => { + const guid = c.req.valid("param").guid; + const user = await prisma.user.findFirst({ + where: { + guid: guid, + }, + select: { guid: true, name: true }, + }); + if (!user) { + return c.json({ message: "User not found" }, 404); + } + + return c.json(200); + }) + + // 特定のユーザーとマッチしたユーザーを取得 + .get("/matched", async (c) => { + const userId = await getUserId(c); + const result = await core.getMatched(userId); + c.status(result.code); + return c.json(result.body); + }) + + // ユーザーにリクエストを送っているユーザーを取得 状態はPENDING + .get("/pending/to-me", async (c) => { + const userId = await getUserId(c); + const sendingUsers = await getPendingRequestsToUser(userId); + c.status(200); + return c.json(sendingUsers); + }) + + // ユーザーがリクエストを送っているユーザーを取得 状態はPENDING + .get("/pending/from-me", async (c) => { + const userId = await getUserId(c); + const receivers = await getPendingRequestsFromUser(userId); + c.status(200); + return c.json(receivers); + }) + + // guidでユーザーを取得 + .get("/guid/:guid", param({ guid: GUIDSchema }), async (c) => { + const guid = c.req.valid("param").guid as GUID; + const user = await getUser(guid); + c.status(200); + return c.json(user); + }) + + // idでユーザーを取得 + .get("/id/:id", param({ id: UserIDSchema }), async (c) => { + const userId = c.req.valid("param").id; + const user = await getUserByID(userId); + c.status(200); + return c.json(user); + }) + + // INSERT INTO "User" VALUES (body...) + .post("/", async (c) => { + const partialUser = InitUserSchema.parse(await c.req.json()); + const user = await createUser(partialUser); + + //ユーザー作成と同時にメモとマッチング + await matchWithMemo(user.id); + c.status(201); + return c.json(user); + }) + + // ユーザーの更新エンドポイント + .put("/me", async (c) => { + const id = await getUserId(c); + const user = UpdateUserSchema.parse(await c.req.json()); + const updated = await updateUser(id, user); + c.status(200); + return c.json(updated); + }) + + // ユーザーの削除エンドポイント + .delete("/me", async (c) => { + const id = await getUserId(c); + await deleteUser(id); + c.status(204); + return c.json({}); + }); export default router; diff --git a/server/src/seeds/data/subjects.ts b/server/src/seeds/data/subjects.ts new file mode 100644 index 00000000..2cfe78ab --- /dev/null +++ b/server/src/seeds/data/subjects.ts @@ -0,0 +1,10 @@ +export const subjects = [ + { + group: "Computer Science", + subjects: ["機械学習", "CPU アーキテクチャ", "型システム", "分散処理"], + }, + { + group: "Math", + subjects: ["Lean4"], + }, +]; diff --git a/server/src/seeds/seed-test.ts b/server/src/seeds/seed-test.ts new file mode 100644 index 00000000..28cf368b --- /dev/null +++ b/server/src/seeds/seed-test.ts @@ -0,0 +1,95 @@ +import { prisma } from "../database/client"; +import { + courses, + enrollments, + interest, + slots, + subjects, + users, +} from "./test-data/data"; + +async function main() { + // avoid flaky tests. don't replace this with Promise.all + for (const { group, subjects: subject } of subjects) { + for (const [name] of subject) { + await prisma.interestSubject.create({ + data: { + name, + group, + }, + }); + } + } + + await Promise.all( + users.map(async (user) => { + await prisma.user.upsert({ + where: { id: user.id }, + update: {}, + create: user, + }); + }), + ); + + await Promise.all( + interest.map(async (interest) => { + await prisma.interest.upsert({ + where: { + userId_subjectId: interest, + }, + update: interest, + create: interest, + }); + }), + ); + + await Promise.all( + courses.map(async (course) => { + await prisma.course.upsert({ + where: { id: course.id }, + update: course, + create: course, + }); + }), + ); + + await Promise.all( + slots.map(async (slot) => { + await prisma.slot.upsert({ + where: { + courseId_period_day: { + courseId: slot.courseId, + period: slot.period, + day: slot.day, + }, + }, + update: slot, + create: slot, + }); + }), + ); + + const promises = enrollments.map(async ([user, course]) => { + await prisma.enrollment.upsert({ + where: { + userId_courseId: { userId: user, courseId: course }, + }, + update: {}, + create: { + userId: user, + courseId: course, + }, + }); + }); + await Promise.all(promises); +} + +await main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/server/src/seeds/seed.ts b/server/src/seeds/seed.ts index 2cb2fe30..fcf36f18 100644 --- a/server/src/seeds/seed.ts +++ b/server/src/seeds/seed.ts @@ -1,243 +1,15 @@ import { prisma } from "../database/client"; +import { subjects } from "./data/subjects"; -async function main() { - // users - await prisma.user.upsert({ - where: { id: 101 }, - update: {}, - create: { - id: 101, - name: "田中太郎", - gender: "男", - grade: "D2", - faculty: "工学部", - department: "電気電子工学科", - intro: "田中太郎です。", - pictureUrl: - "https://firebasestorage.googleapis.com/v0/b/coursemate-tutorial.appspot.com/o/YdulS1s41LVh1nWgOBqzMiXN7803%2FtP5PrelZVe6v4UoF.jpg?alt=media&token=252da169-cccb-45b3-bec6-946ec3de3e27", - guid: "abc101", - }, - }); - - await prisma.user.upsert({ - where: { id: 102 }, - update: {}, - create: { - id: 102, - name: "山田花子", - gender: "女", - grade: "B2", - faculty: "経済学部", - department: "経営学科", - intro: "山田花子です。", - pictureUrl: - "https://firebasestorage.googleapis.com/v0/b/coursemate-tutorial.appspot.com/o/45QiYkH65OWHZYPruT9sHKAHa4I3%2FulavVaTxMNACkcn4.jpg?alt=media&token=6eea4c9f-c9ec-4c6e-943b-96b0afe013c3", - guid: "abc102", - }, - }); - await prisma.user.upsert({ - where: { id: 103 }, - update: {}, - create: { - id: 103, - name: "小五郎", - gender: "男", - grade: "B3", - faculty: "経済学部", - department: "経営学科", - intro: "小五郎です。", - pictureUrl: - "https://firebasestorage.googleapis.com/v0/b/coursemate-tutorial.appspot.com/o/45QiYkH65OWHZYPruT9sHKAHa4I3%2FulavVaTxMNACkcn4.jpg?alt=media&token=6eea4c9f-c9ec-4c6e-943b-96b0afe013c3", - guid: "abc103", - }, - }); // courses - - await prisma.course.upsert({ - where: { id: "10001" }, - update: {}, - create: { - id: "10001", - name: "国語八列", - teacher: "足助太郎", - }, - }); - await prisma.course.upsert({ - where: { id: "10002" }, - update: {}, - create: { - id: "10002", - name: "数学八列", - teacher: "足助太郎", - }, - }); - await prisma.course.upsert({ - where: { id: "10003" }, - update: {}, - create: { - id: "10003", - name: "英語八列", - teacher: "足助太郎", - }, - }); - await prisma.course.upsert({ - where: { id: "10004" }, - update: {}, - create: { - id: "10004", - name: "理科八列", - teacher: "足助太郎", - }, - }); - await prisma.course.upsert({ - where: { id: "10005" }, - update: {}, - create: { - id: "10005", - name: "社会八列", - teacher: "足助太郎", - }, - }); - - // slot - await prisma.slot.upsert({ - where: { - courseId_period_day: { courseId: "10001", period: 4, day: "tue" }, - }, - update: {}, - create: { - courseId: "10001", - day: "tue", - period: 4, - }, - }); - await prisma.slot.upsert({ - where: { - courseId_period_day: { courseId: "10001", period: 4, day: "thu" }, - }, - update: {}, - create: { - courseId: "10001", - day: "thu", - period: 4, - }, - }); - await prisma.slot.upsert({ - where: { - courseId_period_day: { courseId: "10002", period: 3, day: "mon" }, - }, - update: {}, - create: { - courseId: "10002", - day: "mon", - period: 3, - }, - }); - await prisma.slot.upsert({ - where: { - courseId_period_day: { courseId: "10003", period: 3, day: "mon" }, - }, - update: {}, - create: { - courseId: "10003", - day: "mon", - period: 3, - }, - }); - await prisma.slot.upsert({ - where: { - courseId_period_day: { courseId: "10003", period: 3, day: "wed" }, - }, - update: {}, - create: { - courseId: "10003", - day: "wed", - period: 3, - }, - }); - await prisma.slot.upsert({ - where: { - courseId_period_day: { courseId: "10004", period: 3, day: "wed" }, - }, - update: {}, - create: { - courseId: "10004", - day: "wed", - period: 3, - }, - }); - await prisma.slot.upsert({ - where: { - courseId_period_day: { courseId: "10004", period: 3, day: "fri" }, - }, - update: {}, - create: { - courseId: "10004", - day: "fri", - period: 3, - }, - }); - - await prisma.slot.upsert({ - where: { - courseId_period_day: { courseId: "10005", period: 2, day: "tue" }, - }, - update: {}, - create: { - courseId: "10005", - day: "tue", - period: 2, - }, - }); - - await prisma.slot.upsert({ - where: { - courseId_period_day: { courseId: "10005", period: 3, day: "tue" }, - }, - update: {}, - create: { - courseId: "10005", - day: "tue", - period: 3, - }, - }); - - // userId, courseId - const enrollments: Array<[number, string]> = [ - // assert: 101 and 102 has more overlaps in courses than 101 and 103, but less than 102 and 103 - // if you change the assertion above, fix test in engines/recommendation.test.ts too. - [101, "10001"], - [101, "10002"], - [101, "10003"], - [102, "10002"], - [102, "10003"], - [102, "10004"], - [102, "10005"], - [103, "10003"], - [103, "10004"], - [103, "10005"], - ]; - - const promises = enrollments.map(async ([user, course]) => { - await prisma.enrollment.upsert({ +for (const subjectGroup of subjects) { + const group = subjectGroup.group; + for (const name of subjectGroup.subjects) { + await prisma.interestSubject.upsert({ where: { - userId_courseId: { userId: user, courseId: course }, - }, - update: {}, - create: { - userId: user, - courseId: course, + name_group: { name, group }, }, + update: { name, group }, + create: { name, group }, }); - }); - await Promise.all(promises); + } } - -await main() - .then(async () => { - await prisma.$disconnect(); - }) - .catch(async (e) => { - console.error(e); - await prisma.$disconnect(); - process.exit(1); - }); diff --git a/server/src/seeds/test-data/data.ts b/server/src/seeds/test-data/data.ts new file mode 100644 index 00000000..9a97ef9b --- /dev/null +++ b/server/src/seeds/test-data/data.ts @@ -0,0 +1,161 @@ +import type { Day } from "common/types"; + +export const subjects: Array<{ + group: string; + subjects: Array<[string]>; +}> = [ + { + group: "Computer Science", + subjects: [ + ["型システム"], + ["機械学習"], + ["CPU アーキテクチャ"], + ["分散処理"], + ] as const, + }, + { + group: "Math", + subjects: [["Lean4"]], + }, +]; +export const interest = [ + { userId: 101, subjectId: 1 }, + { userId: 102, subjectId: 1 }, + { userId: 103, subjectId: 1 }, + { userId: 101, subjectId: 2 }, + { userId: 101, subjectId: 3 }, + { userId: 102, subjectId: 2 }, + { userId: 102, subjectId: 4 }, + { userId: 103, subjectId: 3 }, + { userId: 103, subjectId: 4 }, +]; + +export const users = [ + { + id: 101, + name: "田中太郎", + gender: "男", + grade: "D2", + faculty: "工学部", + department: "電気電子工学科", + intro: "田中太郎です。", + pictureUrl: + "https://firebasestorage.googleapis.com/v0/b/coursemate-tutorial.appspot.com/o/YdulS1s41LVh1nWgOBqzMiXN7803%2FtP5PrelZVe6v4UoF.jpg?alt=media&token=252da169-cccb-45b3-bec6-946ec3de3e27", + guid: "abc101", + }, + { + id: 102, + name: "山田花子", + gender: "女", + grade: "B2", + faculty: "経済学部", + department: "経営学科", + intro: "山田花子です。", + pictureUrl: + "https://firebasestorage.googleapis.com/v0/b/coursemate-tutorial.appspot.com/o/45QiYkH65OWHZYPruT9sHKAHa4I3%2FulavVaTxMNACkcn4.jpg?alt=media&token=6eea4c9f-c9ec-4c6e-943b-96b0afe013c3", + guid: "abc102", + }, + { + id: 103, + name: "小五郎", + gender: "男", + grade: "B3", + faculty: "経済学部", + department: "経営学科", + intro: "小五郎です。", + pictureUrl: + "https://firebasestorage.googleapis.com/v0/b/coursemate-tutorial.appspot.com/o/45QiYkH65OWHZYPruT9sHKAHa4I3%2FulavVaTxMNACkcn4.jpg?alt=media&token=6eea4c9f-c9ec-4c6e-943b-96b0afe013c3", + guid: "abc103", + }, +]; + +export const courses = [ + { + id: "10001", + name: "国語八列", + teacher: "八十島漕郎", + }, + { + id: "10002", + name: "数学八列", + teacher: "八十島漕郎", + }, + { + id: "10003", + name: "英語八列", + teacher: "八十島漕郎", + }, + { + id: "10004", + name: "理科八列", + teacher: "八十島漕郎", + }, + { + id: "10005", + name: "社会八列", + teacher: "八十島漕郎", + }, +]; + +export const slots: Array<{ courseId: string; day: Day; period: number }> = [ + { + courseId: "10001", + day: "tue", + period: 4, + }, + { + courseId: "10001", + day: "thu", + period: 4, + }, + { + courseId: "10002", + day: "mon", + period: 3, + }, + { + courseId: "10003", + day: "mon", + period: 3, + }, + { + courseId: "10003", + day: "wed", + period: 3, + }, + { + courseId: "10004", + day: "wed", + period: 3, + }, + { + courseId: "10004", + day: "fri", + period: 3, + }, + { + courseId: "10005", + day: "tue", + period: 2, + }, + { + courseId: "10005", + day: "tue", + period: 3, + }, +]; + +export const enrollments: Array<[number, string]> = [ + // assert: 101 and 102 has more overlaps in courses than 101 and 103, but less than 102 and 103 + // if you change the assertion above, fix test in engines/recommendation.test.ts too. + [101, "10001"], + [101, "10002"], + [101, "10003"], + [102, "10002"], + [102, "10003"], + [102, "10004"], + [102, "10005"], + [103, "10003"], + [103, "10004"], + [103, "10005"], +]; diff --git a/server/tsconfig.json b/server/tsconfig.json index ac133f64..20df3ed0 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "target": "es2016", + "noEmit": true, "module": "commonjs", "rootDir": "./src", "outDir": "./target", diff --git a/test/fetcher.ts b/test/fetcher.ts index 8a6fc0be..8bfe804b 100644 --- a/test/fetcher.ts +++ b/test/fetcher.ts @@ -1,34 +1,31 @@ +import app from "../server/src/index"; type Config = Omit; -function prefix(path: string): string { - if (path.at(0) === "/") return `localhost:3000${path}`; - return path; -} -export async function GET(path: string, config?: Config) { - return await fetch(prefix(path), { +export async function GET(path: T, config?: Config) { + return await app.request(path, { ...config, }); } -export async function POST(path: string, config?: Config) { - return await fetch(prefix(path), { +export async function POST(path: T, config?: Config) { + return await app.request(path, { method: "POST", ...config, }); } -export async function PUT(path: string, config?: Config) { - return await fetch(prefix(path), { +export async function PUT(path: T, config?: Config) { + return await app.request(path, { method: "PUT", ...config, }); } -export async function PATCH(path: string, config?: Config) { - return await fetch(prefix(path), { +export async function PATCH(path: T, config?: Config) { + return await app.request(path, { method: "PATCH", ...config, }); } -export async function DELETE(path: string, config?: Config) { - return await fetch(prefix(path), { +export async function DELETE(path: T, config?: Config) { + return await app.request(path, { method: "DELETE", ...config, }); diff --git a/test/server.spec.ts b/test/server.spec.ts index 6d4c77ac..ded40ce1 100644 --- a/test/server.spec.ts +++ b/test/server.spec.ts @@ -1,10 +1,6 @@ -import { afterAll, beforeAll, expect, test } from "bun:test"; -import type { Server } from "node:http"; -import { main } from "../server/src/index"; +import { beforeAll, expect, test } from "bun:test"; import { GET, PUT } from "./fetcher"; -let server: Server; - const MOCK_TOKEN = "I_AM_abc101"; beforeAll(() => { @@ -15,17 +11,12 @@ beforeAll(() => { ); throw new Error(`got: \`${DATABASE_URL}\``); } - server = main(); -}); - -afterAll(() => { - server.close(); }); test("server up", async () => { const res = await GET("/"); const text = await res.text(); - expect(text).toBe(`"Hello from Express!"`); + expect(text).toBe("Hello from Hono 🔥"); }); test("/users/exists", async () => { @@ -38,7 +29,11 @@ test("/users/exists", async () => { test("basic auth", async () => { let res = await GET("/users/me"); expect(res.status).toBe(401); - res = await GET(`/users/me?token=${MOCK_TOKEN}`); + res = await GET("/users/me", { + headers: { + Authorization: MOCK_TOKEN, + }, + }); expect(res.status).toBe(200); const json = await res.json(); expect(json.name).toBe("田中太郎"); @@ -48,18 +43,30 @@ test("send request", async () => { // should error in auth let res = await GET("/users/pending/from-me"); expect(res.status).toBe(401); - // should error in auth res = await PUT("/requests/send/102"); expect(res.status).toBe(401); - res = await GET(`/users/pending/from-me?token=${MOCK_TOKEN}`); + res = await GET("/users/pending/from-me", { + headers: { + Authorization: MOCK_TOKEN, + }, + }); + expect(res.status).toBe(200); expect(await res.json()).toSatisfy((s) => s.length === 0); // starting actual request - res = await PUT(`/requests/send/102?token=${MOCK_TOKEN}`); + res = await PUT("/requests/send/102", { + headers: { + Authorization: MOCK_TOKEN, + }, + }); expect(res.status).toBe(201); - res = await GET(`/users/pending/from-me?token=${MOCK_TOKEN}`); + res = await GET("/users/pending/from-me", { + headers: { + Authorization: MOCK_TOKEN, + }, + }); expect(await res.json()).toSatisfy( (s) => s.length === 1 && s[0].name === "山田花子", ); diff --git a/web/.env.sample b/web/.env.sample index 05df56ba..a514b4d3 100644 --- a/web/.env.sample +++ b/web/.env.sample @@ -1,13 +1,14 @@ -VITE_API_ENDPOINT=http://localhost:3000 -VITE_FIREBASE_API_KEY=example -VITE_FIREBASE_AUTH_DOMAIN=example.com -VITE_FIREBASE_PROJECT_ID=example -# VITE_FIREBASE_STORAGE_BUCKET=example.com -# VITE_FIREBASE_MESSAGING_SENDER_ID=example -VITE_FIREBASE_APP_ID=example -VITE_FIREBASE_MEASUREMENT_ID=example +NEXT_PUBLIC_API_ENDPOINT=http://localhost:3000 +NEXT_PUBLIC_FIREBASE_API_KEY=example +NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=example.com +NEXT_PUBLIC_FIREBASE_PROJECT_ID=example +# NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=example.com +# NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=example +NEXT_PUBLIC_FIREBASE_APP_ID=example +NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=example +NEXT_PUBLIC_ALLOW_ANY_MAIL_ADDR=true #supabase -VITE_SUPABASE_URL= -VITE_SUPABASE_KEY= +NEXT_PUBLIC_SUPABASE_URL= +NEXT_PUBLIC_SUPABASE_KEY= diff --git a/web/.gitignore b/web/.gitignore index a547bf36..29bec2fa 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -22,3 +22,11 @@ dist-ssr *.njsproj *.sln *.sw? + +.next +next-env.d.ts +out + +/common + +tsconfig.tsbuildinfo diff --git a/web/Dockerfile b/web/Dockerfile index 458eef88..7a223471 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /coursemate/dev/web COPY package.json package-lock.json ./ RUN npm ci -ENV VITE_API_ENDPOINT=http://localhost:3000 +ENV NEXT_PUBLIC_API_ENDPOINT=http://localhost:3000 COPY ./ . CMD npm run dev -- --host diff --git a/web/README.md b/web/README.md deleted file mode 100644 index bb156850..00000000 --- a/web/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - -- Configure the top-level `parserOptions` property like this: - -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - project: ["./tsconfig.json", "./tsconfig.node.json"], - tsconfigRootDir: __dirname, - }, -}; -``` - -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/web/src/api/chat/chat.ts b/web/api/chat/chat.ts similarity index 78% rename from web/src/api/chat/chat.ts rename to web/api/chat/chat.ts index 182ee85a..1960de73 100644 --- a/web/src/api/chat/chat.ts +++ b/web/api/chat/chat.ts @@ -3,19 +3,21 @@ import type { InitRoom, Message, MessageID, + PersonalizedDMRoom, + RelationshipID, RoomOverview, SendMessage, ShareRoomID, SharedRoom, UpdateRoom, UserID, -} from "../../common/types"; -import { ErrUnauthorized, credFetch } from "../../firebase/auth/lib"; -import endpoints from "../internal/endpoints"; +} from "common/types"; +import { ErrUnauthorized, credFetch } from "~/firebase/auth/lib"; +import * as endpoints from "../internal/endpoints"; /* TODO -import { UserID } from "../common/types"; -import type { User } from "../common/types"; +import { UserID } from "common/types"; +import type { User } from "common/types"; */ //// DM グループチャット 共通//// @@ -34,6 +36,20 @@ export async function deleteMessage( ); } +export async function markAsRead( + relationId: RelationshipID, + messageId: MessageID, +) { + const res = await credFetch( + "POST", + endpoints.markAsRead(relationId, messageId), + ); + if (res.status !== 200 && res.status !== 304) + throw new Error( + `on markAsRead(), expected status code of 200 or 304, but got ${res.status}`, + ); +} + export async function updateMessage( messageId: MessageID, newMessage: SendMessage, @@ -54,7 +70,16 @@ export async function updateMessage( export async function overview(): Promise { const res = await credFetch("GET", endpoints.roomOverview); if (res.status === 401) throw new ErrUnauthorized(); - return await res.json(); + const json: RoomOverview[] = await res.json(); + + if (!Array.isArray(json)) return json; + + for (const room of json) { + if (!room.lastMsg) continue; + room.lastMsg.createdAt = new Date(room.lastMsg.createdAt); + } + + return json; } //// DM関連 //// @@ -74,15 +99,21 @@ export async function sendDM( return res.json(); } -// WARNING: don't use this outside of api/ -export async function getDM(friendId: UserID): Promise { +export async function getDM( + friendId: UserID, +): Promise { const res = await credFetch("GET", endpoints.dmWith(friendId)); if (res.status === 401) throw new ErrUnauthorized(); if (res.status !== 200) throw new Error( `getDM() failed: expected status code 200, got ${res.status}`, ); - return res.json(); + const json: DMRoom & PersonalizedDMRoom = await res.json(); + if (!Array.isArray(json?.messages)) return json; + for (const m of json.messages) { + m.createdAt = new Date(m.createdAt); + } + return json; } ////グループチャット関連//// diff --git a/web/src/api/chat/hooks.ts b/web/api/chat/hooks.ts similarity index 59% rename from web/src/api/chat/hooks.ts rename to web/api/chat/hooks.ts index d7737afb..ecf35598 100644 --- a/web/src/api/chat/hooks.ts +++ b/web/api/chat/hooks.ts @@ -1,16 +1,22 @@ +"use client"; + +// import { useCallback, useEffect, useState } from "react"; +import type { Message, RoomOverview } from "common/types"; +import { MessageSchema, RoomOverviewSchema } from "common/zod/schemas"; import { useCallback } from "react"; import { z } from "zod"; -// import { useCallback, useEffect, useState } from "react"; -import type { Message, RoomOverview } from "../../common/types"; -import { MessageSchema, RoomOverviewSchema } from "../../common/zod/schemas"; -import { type Hook, useSWR } from "../../hooks/useSWR"; +import { type Hook, useCustomizedSWR } from "~/hooks/useCustomizedSWR"; import type { UserID } from "../internal/endpoints"; -// import type { Hook } from "../share/types"; +// import type { Hook } from "~/share/types"; import * as chat from "./chat"; const OverviewListSchema = z.array(RoomOverviewSchema); export function useRoomsOverview(): Hook { - return useSWR("useRoomsOverview", chat.overview, OverviewListSchema); + return useCustomizedSWR( + "useRoomsOverview", + chat.overview, + OverviewListSchema, + ); } const MessageListSchema = z.array(MessageSchema); @@ -21,5 +27,5 @@ export function useMessages(friendId: UserID): Hook { async () => (await chat.getDM(friendId)).messages, [friendId], ); - return useSWR(key, fetcher, MessageListSchema); + return useCustomizedSWR(key, fetcher, MessageListSchema); } diff --git a/web/api/course.hook.ts b/web/api/course.hook.ts new file mode 100644 index 00000000..cf774c53 --- /dev/null +++ b/web/api/course.hook.ts @@ -0,0 +1,10 @@ +import type { Course } from "common/types"; +import { CourseSchema } from "common/zod/schemas"; +import { z } from "zod"; +import { type Hook, useCustomizedSWR } from "~/hooks/useCustomizedSWR"; +import { getMyCourses } from "./course"; + +const CourseListSchema = z.array(CourseSchema); +export function useMyCourses(): Hook { + return useCustomizedSWR("useMyCourses", getMyCourses, CourseListSchema); +} diff --git a/web/src/api/course.ts b/web/api/course.ts similarity index 95% rename from web/src/api/course.ts rename to web/api/course.ts index bc4bd9d7..e50ef45b 100644 --- a/web/src/api/course.ts +++ b/web/api/course.ts @@ -1,5 +1,5 @@ -import type { Course, CourseID, Day } from "../common/types"; -import { credFetch } from "../firebase/auth/lib"; +import type { Course, CourseID, Day } from "common/types"; +import { credFetch } from "~/firebase/auth/lib"; import endpoints from "./internal/endpoints"; // TODO: migrate to safe functions diff --git a/web/api/image.ts b/web/api/image.ts new file mode 100644 index 00000000..2f5cb478 --- /dev/null +++ b/web/api/image.ts @@ -0,0 +1,14 @@ +import type { UserID } from "common/types"; +import * as endpoints from "./internal/endpoints"; +import { uploadImage as uploader } from "./internal/fetch-func"; +export { MAX_IMAGE_SIZE } from "./internal/fetch-func"; + +export async function uploadAvatar(f: File) { + return await uploader(endpoints.profilePicture, f); +} + +/** @throws if failed to send image. **/ +export async function sendImageTo(u: UserID, f: File) { + console.log("sendImageTo"); + await uploader(endpoints.sendPictureTo(u), f); +} diff --git a/web/src/api/internal/endpoints.ts b/web/api/internal/endpoints.ts similarity index 63% rename from web/src/api/internal/endpoints.ts rename to web/api/internal/endpoints.ts index aeb1c67e..e10c6717 100644 --- a/web/src/api/internal/endpoints.ts +++ b/web/api/internal/endpoints.ts @@ -1,8 +1,10 @@ -import type { CourseID, Day, GUID } from "../../common/types"; -import type { MessageID, ShareRoomID } from "../../common/types"; +import { panic } from "common/lib/panic"; +import type { CourseID, Day, GUID } from "common/types"; +import type { MessageID, ShareRoomID } from "common/types"; -export const origin: string | null = import.meta.env.VITE_API_ENDPOINT; -if (!origin) throw new Error("import.meta.env.VITE_API_ENDPOINT not found!"); +export const API_ENDPOINT: string = + process.env.NEXT_PUBLIC_API_ENDPOINT ?? + panic("process.env.NEXT_PUBLIC_API_ENDPOINT not found!"); // TODO: de-export this and use one from /common export type UserID = number; @@ -12,12 +14,12 @@ export type UserID = number; * GET -> get user's info. TODO: filter return info by user's options and open level. * - statuses: * - 200: ok. - * - body: User + * - body: UserWithCoursesAndSubjects * - 400: not found. * - 500: internal error. **/ export const user = (userId: UserID) => { - return `${origin}/users/id/${userId}`; + return `${API_ENDPOINT}/users/id/${userId}`; }; /** @@ -36,18 +38,18 @@ export const user = (userId: UserID) => { * - 500: internal error. * **/ -export const users = `${origin}/users`; +export const users = `${API_ENDPOINT}/users`; /** * [v] 実装済み * GET -> get top N users recommended to me. * - statuses: * - 200: good. - * - body: User[] + * - body: UserWithCoursesAndSubjects[] * - 401: auth error. * - 500: internal error **/ -export const recommendedUsers = `${origin}/users/recommended`; +export const recommendedUsers = `${API_ENDPOINT}/users/recommended`; /** * [v] 実装済み @@ -62,7 +64,7 @@ export const recommendedUsers = `${origin}/users/recommended`; * - request body: Omit * - statuses: * - 200: ok. - * - body: User + * - body: UserWithCoursesAndSubjects * - 500: internal error. * * [v] 実装済み @@ -72,52 +74,52 @@ export const recommendedUsers = `${origin}/users/recommended`; * - 500: internal error. * **/ -export const me = `${origin}/users/me`; +export const me = `${API_ENDPOINT}/users/me`; /** * [v] 実装済み * GET -> list all matched users. * - statuses: * - 200: ok. - * - body: User[] + * - body: UserWithCoursesAndSubjects[] * - 401: unauthorized. * - 500: internal error. **/ -export const matchedUsers = `${origin}/users/matched`; +export const matchedUsers = `${API_ENDPOINT}/users/matched`; /** * [v] 実装済み * GET -> list all users that sent request to you. * - statuses: * - 200: ok. - * - body: User[] + * - body: UserWithCoursesAndSubjects[] * - 401: unauthorized. * - 500: internal error. **/ -export const pendingRequestsToMe = `${origin}/users/pending/to-me`; +export const pendingRequestsToMe = `${API_ENDPOINT}/users/pending/to-me`; /** * [v] 実装済み * GET -> list all users that you sent request. * - statuses: * - 200: ok. - * - body: User[] + * - body: UserWithCoursesAndSubjects[] * - 401: unauthorized. * - 500: internal error. **/ -export const pendingRequestsFromMe = `${origin}/users/pending/from-me`; +export const pendingRequestsFromMe = `${API_ENDPOINT}/users/pending/from-me`; /** * [v] 実装済み * GET -> get user's info. TODO: filter return info by user's options and open level. * - statuses: * - 200: ok. - * - body: User + * - body: UserWithCoursesAndSubjects * - 400: not found. * - 500: internal error. **/ export const userByGUID = (guid: GUID) => { - return `${origin}/users/guid/${guid}`; + return `${API_ENDPOINT}/users/guid/${guid}`; }; // this one may be public to anyone. @@ -126,11 +128,12 @@ export const userByGUID = (guid: GUID) => { * GET -> check if the user exists. * - statuses: * - 200: yes, user exists. + * - body: UserWithCoursesAndSubjects * - 404: no, user doesn't exist. * - 500: internal error. **/ export const userExists = (guid: GUID) => { - return `${origin}/users/exists/${guid}`; + return `${API_ENDPOINT}/users/exists/${guid}`; }; /** @@ -144,7 +147,7 @@ export const userExists = (guid: GUID) => { * - 500: internal error. **/ export const match = (opponentID: UserID) => { - return `${origin}/matches/${opponentID}`; + return `${API_ENDPOINT}/matches/${opponentID}`; }; /** @@ -157,7 +160,7 @@ export const match = (opponentID: UserID) => { * - 401: unauthorized. * - 500: internal error. **/ -export const matches = `${origin}/matches`; +export const matches = `${API_ENDPOINT}/matches`; /** * [x] 実装済み @@ -179,7 +182,7 @@ export const matches = `${origin}/matches`; * - 500: internal error. */ export const coursesUserId = (userId: UserID) => { - return `${origin}/courses/userId/${userId}`; + return `${API_ENDPOINT}/courses/userId/${userId}`; }; /** @@ -205,7 +208,7 @@ export const coursesUserId = (userId: UserID) => { * - 401: unauthorized. * - 500: internal error. */ -export const coursesMine = `${origin}/courses/mine`; +export const coursesMine = `${API_ENDPOINT}/courses/mine`; /** * [v] 実装済み @@ -217,7 +220,7 @@ export const coursesMine = `${origin}/courses/mine`; * - 500: internal error. */ export const coursesMineOverlaps = (courseId: CourseID) => { - return `${origin}/courses/mine/overlaps/${courseId}`; + return `${API_ENDPOINT}/courses/mine/overlaps/${courseId}`; }; /** @@ -230,9 +233,76 @@ export const coursesMineOverlaps = (courseId: CourseID) => { * - 500: internal error. **/ export const coursesDayPeriod = (day: Day, period: number) => { - return `${origin}/courses/day-period?day=${day}&period=${period}`; + return `${API_ENDPOINT}/courses/day-period?day=${day}&period=${period}`; +}; + +/** + * [v] 実装済み + * POST -> create a new subject. + * - statuses: + * - 200: ok. + * - body: InterestSubject + * - 401: unauthorized. + * - 500: internal error. + */ +export const subjects = `${API_ENDPOINT}/subjects`; + +/** + * [v] 実装済み + * GET -> get subjects the user is interested in. + * - statuses: + * - 200: ok. + * - body: InterestSubject[] + * - 401: unauthorized. + * - 500: internal error. + */ +export const subjectsUserId = (userId: UserID) => { + return `${API_ENDPOINT}/subjects/userId/${userId}`; }; +/** + * [v] 実装済み + * GET -> get my subjects. + * - statuses: + * - 200: ok. + * - body: InterestSubject[] + * - 401: unauthorized. + * - 500: internal error. + * PATCH -> update my subjects. + * - request body: SubjectId + * - statuses: + * - 200: ok. + * - body: InterestSubject[] + * - 401: unauthorized. + * - 500: internal error. + * DELETE -> delete my subjects. + * - request body: SubjectId + * - statuses: + * - 200: ok. + * - body: InterestSubject[] + * - 401: unauthorized. + * - 500: internal error. + * PUT → replace my subjects. + * - request body: SubjectId[] + * - statuses: + * - 200: ok. + * - body: InterestSubject[] + * - 401: unauthorized. + * - 500: internal error. + */ +export const subjectsMine = `${API_ENDPOINT}/subjects/mine`; + +/** + * [v] 実装済み + * GET -> get all subjects. + * - statuses: + * - 200: ok. + * - body: InterestSubject[] + * - 401: unauthorized. + * - 500: internal error. + */ +export const subjectsAll = `${API_ENDPOINT}/subjects/all`; + /** * [v] 実装済み * PUT -> create request. @@ -242,7 +312,7 @@ export const coursesDayPeriod = (day: Day, period: number) => { * - 500: internal error. **/ export const sendRequest = (opponentId: UserID) => { - return `${origin}/requests/send/${opponentId}`; + return `${API_ENDPOINT}/requests/send/${opponentId}`; }; /** @@ -253,7 +323,7 @@ export const sendRequest = (opponentId: UserID) => { * - 500: internal error **/ export const cancelRequest = (opponentId: UserID) => { - return `${origin}/requests/cancel/${opponentId}`; + return `${API_ENDPOINT}/requests/cancel/${opponentId}`; }; /** * [v] 実装済み @@ -264,7 +334,7 @@ export const cancelRequest = (opponentId: UserID) => { * - 500: internal error. **/ export const acceptRequest = (opponentId: UserID) => { - return `${origin}/requests/accept/${opponentId}`; + return `${API_ENDPOINT}/requests/accept/${opponentId}`; }; /** @@ -277,9 +347,14 @@ export const acceptRequest = (opponentId: UserID) => { * - 500: internal error. **/ export const rejectRequest = (opponentId: UserID) => { - return `${origin}/requests/reject/${opponentId}`; + return `${API_ENDPOINT}/requests/reject/${opponentId}`; }; +/** + **/ +export const markAsRead = (friendId: UserID, messageId: MessageID) => { + return `${API_ENDPOINT}/chat/mark-as-read/${friendId}/${messageId}`; +}; /** * []実装済み * GET -> get personalized room overviews. @@ -289,7 +364,7 @@ export const rejectRequest = (opponentId: UserID) => { * - 401: unauthorized * - 500: internal error */ -export const roomOverview = `${origin}/chat/overview`; +export const roomOverview = `${API_ENDPOINT}/chat/overview`; /** * []実装済み @@ -302,7 +377,7 @@ export const roomOverview = `${origin}/chat/overview`; * - 403: Forbidden * - 500: internal error **/ -export const dmTo = (userId: UserID) => `${origin}/chat/dm/to/${userId}`; +export const dmTo = (userId: UserID) => `${API_ENDPOINT}/chat/dm/to/${userId}`; /** * PUT -> start dm with userId. created one if none was found. authorized. @@ -316,7 +391,8 @@ export const dmTo = (userId: UserID) => `${origin}/chat/dm/to/${userId}`; * - 403: forbidden. you and the user are not matched yet. * - 500: internal error. **/ -export const dmWith = (userId: UserID) => `${origin}/chat/dm/with/${userId}`; +export const dmWith = (userId: UserID) => + `${API_ENDPOINT}/chat/dm/with/${userId}`; /** * POST -> Create a room. authenticated @@ -328,7 +404,7 @@ export const dmWith = (userId: UserID) => `${origin}/chat/dm/with/${userId}`; * - 403: forbidden (cannot invite non-friends) * - 500: internal error **/ -export const sharedRooms = `${origin}/chat/shared`; +export const sharedRooms = `${API_ENDPOINT}/chat/shared`; /** authorized * GET -> Get info of a room (including the message log). @@ -336,7 +412,7 @@ export const sharedRooms = `${origin}/chat/shared`; * - body: UpdateRoom **/ export const sharedRoom = (roomId: ShareRoomID) => - `${origin}/chat/shared/${roomId}`; + `${API_ENDPOINT}/chat/shared/${roomId}`; /** POST: invite. authorized * - body: UserID[] @@ -347,24 +423,31 @@ export const sharedRoom = (roomId: ShareRoomID) => * - 500: internal error **/ export const roomInvite = (roomId: ShareRoomID) => - `${origin}/chat/shared/id/${roomId}/invite`; + `${API_ENDPOINT}/chat/shared/id/${roomId}/invite`; /** * PATCH: authorized body=SendMessage * DELETE: authorized **/ export const message = (messageId: MessageID) => - `${origin}/chat/messages/id/${messageId}`; + `${API_ENDPOINT}/chat/messages/id/${messageId}`; +/** + * POST: send picture. + */ +export const sendPictureTo = (friendId: UserID) => + `${API_ENDPOINT}/picture/to/${friendId}`; /** * GET: get profile picture of URL (this is usually hard-encoded in pictureURL so this variable is barely used) */ -export const pictureOf = (guid: GUID) => `${origin}/picture/${guid}`; +export const profilePictureOf = (guid: GUID) => + `${API_ENDPOINT}/picture/profile/${guid}`; +export const pictureOf = (guid: GUID) => `${API_ENDPOINT}/picture/${guid}`; /** * POST: update my profile picture. */ -export const picture = `${origin}/picture`; +export const profilePicture = `${API_ENDPOINT}/picture/profile`; export default { user, @@ -384,6 +467,10 @@ export default { cancelRequest, coursesUserId, coursesDayPeriod, + subjects, + subjectsUserId, + subjectsMine, + subjectsAll, roomOverview, dmTo, dmWith, @@ -393,6 +480,6 @@ export default { message, coursesMine, coursesMineOverlaps, - pictureOf, - picture, + profilePictureOf, + profilePicture, }; diff --git a/web/api/internal/fetch-func.ts b/web/api/internal/fetch-func.ts new file mode 100644 index 00000000..69663bd5 --- /dev/null +++ b/web/api/internal/fetch-func.ts @@ -0,0 +1,24 @@ +import { getIdToken } from "~/firebase/auth/lib"; + +type URL = string; + +export const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB + +export async function uploadImage(path: string, file: File): Promise { + if (file.size >= MAX_IMAGE_SIZE) { + throw new Error("画像のアップロードに失敗しました: 画像が大きすぎます"); + } + const res = await fetch(path, { + method: "POST", + headers: { + "Content-Type": "image/png", + Authorization: await getIdToken(), + }, + body: file, + }); + if (res.status !== 201) + throw new Error( + `Unexpected status code: expected 201, but got ${res.status}`, + ); + return await res.text(); +} diff --git a/web/src/api/match.ts b/web/api/match.ts similarity index 89% rename from web/src/api/match.ts rename to web/api/match.ts index f9496afe..0751c11f 100644 --- a/web/src/api/match.ts +++ b/web/api/match.ts @@ -1,4 +1,4 @@ -import { credFetch } from "../firebase/auth/lib"; +import { credFetch } from "~/firebase/auth/lib"; import endpoints from "./internal/endpoints"; import type { UserID } from "./internal/endpoints"; diff --git a/web/src/api/request.ts b/web/api/request.ts similarity index 94% rename from web/src/api/request.ts rename to web/api/request.ts index efb20dcb..aaecadad 100644 --- a/web/src/api/request.ts +++ b/web/api/request.ts @@ -1,4 +1,4 @@ -import { credFetch } from "../firebase/auth/lib"; +import { credFetch } from "~/firebase/auth/lib"; import endpoints, { type UserID } from "./internal/endpoints"; //指定したユーザにリクエストを送る diff --git a/web/src/api/share/types.ts b/web/api/share/types.ts similarity index 100% rename from web/src/api/share/types.ts rename to web/api/share/types.ts diff --git a/web/api/subject.ts b/web/api/subject.ts new file mode 100644 index 00000000..8b49764f --- /dev/null +++ b/web/api/subject.ts @@ -0,0 +1,49 @@ +import type { + InterestSubject, + InterestSubjectID, + UserID, +} from "common/types.ts"; +import { InterestSubjectSchema } from "common/zod/schemas.ts"; +import { z } from "zod"; +import { credFetch } from "../firebase/auth/lib.ts"; +import { type Hook, useCustomizedSWR } from "../hooks/useCustomizedSWR.ts"; +import endpoints from "./internal/endpoints.ts"; + +const InterestSubjectListSchema = z.array(InterestSubjectSchema); + +// 興味分野を作成する +export async function create(name: string) { + return await credFetch("POST", endpoints.subjects, { name }); +} + +// 自身の興味分野を取得する +export function useMyInterests(): Hook { + return useCustomizedSWR( + "interests::mine", + getMySubjects, + InterestSubjectListSchema, + ); +} + +async function getMySubjects(): Promise { + const res = await credFetch("GET", endpoints.subjectsMine); + return res.json(); +} + +// 自身の興味分野を更新する +export async function update(newSubjectIds: InterestSubjectID[]) { + const url = endpoints.subjectsMine; + return await credFetch("PUT", url, { subjectIds: newSubjectIds }); +} + +// 指定した userId のユーザの興味分野を取得 +export async function get(id: UserID): Promise { + const res = await credFetch("GET", endpoints.subjectsUserId(id)); + return await res.json(); +} + +// すべての興味分野を取得 +export async function getAll(): Promise { + const res = await credFetch("GET", endpoints.subjectsAll); + return await res.json(); +} diff --git a/web/src/api/user.ts b/web/api/user.ts similarity index 60% rename from web/src/api/user.ts rename to web/api/user.ts index 64315258..923900fb 100644 --- a/web/src/api/user.ts +++ b/web/api/user.ts @@ -1,55 +1,80 @@ +import type { + GUID, + UpdateUser, + User, + UserID, + UserWithCoursesAndSubjects, +} from "common/types.ts"; +import { parseUser } from "common/zod/methods.ts"; +import { + UserIDSchema, + UserWithCoursesAndSubjectsSchema, +} from "common/zod/schemas.ts"; import { z } from "zod"; -import type { GUID, UpdateUser, User, UserID } from "../common/types"; -import { parseUser } from "../common/zod/methods.ts"; -import { UserIDSchema, UserSchema } from "../common/zod/schemas.ts"; -import { credFetch } from "../firebase/auth/lib.ts"; -import { useAuthorizedData } from "../hooks/useData.ts"; -import { type Hook, useSWR } from "../hooks/useSWR.ts"; +import { credFetch } from "~/firebase/auth/lib.ts"; +import { type Hook, useCustomizedSWR } from "~/hooks/useCustomizedSWR.ts"; +import { useAuthorizedData } from "~/hooks/useData.ts"; import endpoints from "./internal/endpoints.ts"; import type { Hook as UseHook } from "./share/types.ts"; -const UserListSchema = z.array(UserSchema); +const UserListSchema = z.array(UserWithCoursesAndSubjectsSchema); -export function useRecommended(): UseHook { +export function useAll(): Hook { + return useCustomizedSWR("users::all", all, UserListSchema); +} +export function useRecommended(): UseHook { const url = endpoints.recommendedUsers; - return useAuthorizedData(url); + return useAuthorizedData(url, UserListSchema); +} +export function useMatched(): Hook { + return useCustomizedSWR("users::matched", matched, UserListSchema); } -export function useMatched(): Hook { - return useSWR("users::matched", matched, UserListSchema); +export function usePendingToMe(): Hook { + return useCustomizedSWR("users::pending::to-me", pendingToMe, UserListSchema); } -export function usePendingToMe(): Hook { - return useSWR("users::pending::to-me", pendingToMe, UserListSchema); +export function usePendingFromMe(): Hook { + return useCustomizedSWR( + "users::pending::from-me", + pendingFromMe, + UserListSchema, + ); } -export function usePendingFromMe(): Hook { - return useSWR("users::pending::from-me", pendingFromMe, UserListSchema); + +async function all(): Promise { + const res = await credFetch("GET", endpoints.users); + return res.json(); } -async function matched(): Promise { +async function matched(): Promise { const res = await credFetch("GET", endpoints.matchedUsers); return res.json(); } -async function pendingToMe(): Promise { +async function pendingToMe(): Promise { const res = await credFetch("GET", endpoints.pendingRequestsToMe); return await res.json(); } -async function pendingFromMe(): Promise { +async function pendingFromMe(): Promise { const res = await credFetch("GET", endpoints.pendingRequestsFromMe); return await res.json(); } // 自身のユーザー情報を取得する -export function useAboutMe(): Hook { - return useSWR("users::aboutMe", aboutMe, UserSchema); +export function useAboutMe(): Hook { + return useCustomizedSWR( + "users::aboutMe", + aboutMe, + UserWithCoursesAndSubjectsSchema, + ); } -async function aboutMe(): Promise { +async function aboutMe(): Promise { const res = await credFetch("GET", endpoints.me); return res.json(); } // 自身のユーザーIDを取得する export function useMyID(): Hook { - return useSWR("users::myId", getMyId, UserIDSchema); + return useCustomizedSWR("users::myId", getMyId, UserIDSchema); } async function getMyId(): Promise { const me = await aboutMe(); @@ -127,6 +152,8 @@ export async function deleteAccount(): Promise { const res = await credFetch("DELETE", endpoints.me); if (res.status !== 204) throw new Error( - `failed to delete account: expected status code 204, but got ${res.status} with text ${await res.text()}`, + `failed to delete account: expected status code 204, but got ${ + res.status + } with text ${await res.text()}`, ); } diff --git a/web/app/chat/components/MessageInput.tsx b/web/app/chat/components/MessageInput.tsx new file mode 100644 index 00000000..693f28dd --- /dev/null +++ b/web/app/chat/components/MessageInput.tsx @@ -0,0 +1,111 @@ +import ImageIcon from "@mui/icons-material/Image"; +import { sendImageTo } from "~/api/image"; + +import type { DMOverview, SendMessage, UserID } from "common/types"; +import { parseContent } from "common/zod/methods"; +import { useEffect, useState } from "react"; +import { MdSend } from "react-icons/md"; + +type Props = { + send: (to: UserID, m: SendMessage) => void; + reload: () => void; + room: DMOverview; +}; + +const crossRoomMessageState = new Map(); + +export function MessageInput({ reload, send, room }: Props) { + const friendId = room.friendId; + const [message, _setMessage] = useState(""); + const [error, setError] = useState(null); + const isMatched = room.matchingStatus === "matched"; + + function setMessage(m: string) { + _setMessage(m); + crossRoomMessageState.set(friendId, m); + } + + useEffect(() => { + _setMessage(crossRoomMessageState.get(friendId) || ""); + }, [friendId]); + + function submit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + + try { + parseContent(message); + } catch { + return; + } + + if (message.trim()) { + send(friendId, { content: message }); + setMessage(""); + } + } + + function handleKeyDown(e: React.KeyboardEvent) { + if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { + e.preventDefault(); + setError(null); + + try { + parseContent(message); + } catch { + return; + } + if (message.trim()) { + send(friendId, { content: message }); + setMessage(""); + } + } + } + + return ( +
+
+
+ {isMatched && ( + + )} +