diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..95f0be4 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +DATABASE_URL=postgres://user:pass@host:5432/chess +GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com +STRIPE_SECRET_KEY=sk_test_... +STRIPE_WEBHOOK_SECRET=whsec_... +STRIPE_PRICE_PRO=price_... +APP_URL=http://localhost:3000 +PORT=3000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5423785 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: ci + +on: + push: + pull_request: + +jobs: + api-build-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: package-lock.json + + - name: Install API deps + run: npm ci + + - name: Build API + run: npm run build + + - name: Test API + run: npm test + + web-build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: web/package-lock.json + + - name: Install web deps + run: npm ci + + - name: Build web + run: npm run build diff --git a/.gitignore b/.gitignore index 37d7e73..896aacb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules .env +dist +*.log diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..f8b6d4b --- /dev/null +++ b/PLAN.md @@ -0,0 +1,75 @@ +# CHESS API SAAS PLAN (Execution Checklist) + +## Goal +Ship a production-ready SaaS chess API where users can sign in, create API keys, and build apps without their own backend. + +## Current State (done) +- [x] NestJS modular rewrite (auth/users/chess/database/health) +- [x] Postgres + Drizzle schemas +- [x] Google auth endpoint +- [x] API key create/list/revoke +- [x] Core chess routes (create/get/delete/moves/ai/resign) +- [x] Timed games support +- [x] DTO coverage + module tests + engine tests +- [x] SaaS landing/docs/login/dashboard scaffolding + wiring +- [x] Basic plan model + free-tier API key limits + +## Remaining (must finish) + +### 1) Billing + Entitlements (Stripe) +- [x] Stripe customer creation on first paid action +- [x] Checkout session endpoint +- [x] Billing portal endpoint +- [x] Webhook handler (subscription created/updated/canceled) +- [x] Persist plan tier/status in `plans` +- [x] Enforce plan limits: + - [x] free: low RPM + low monthly requests + key cap + - [x] pro: higher quotas + +### 2) Production API hardening +- [x] Centralized exception filter + error shape +- [x] Request logging middleware with requestId +- [x] API key rate limit per key (not only global) +- [x] Pagination DTOs + list endpoints: + - [x] `GET /games` with filters/status/mode + - [x] `GET /players` (or profile-scoped player listing) +- [x] Ownership checks on all mutable resources + +### 3) Data model completion +- [x] `players` table (profile for app users) +- [x] `game_players` table (white/black participants) +- [x] Optional metadata fields for external app IDs + +### 4) Docs + Developer onboarding +- [x] “Get key in 60 seconds” flow docs +- [x] Copy-paste SDK-style snippets (JS/TS + curl) +- [x] Plan limits docs + upgrade path +- [x] Error code catalog + +### 5) Ops + deployment +- [x] docker-compose for local Postgres + app +- [x] env validation at boot +- [x] CI workflow (build + test) +- [x] Vercel deployment files/config +- [x] staging verification script + +### 6) QA checklist (before STG signoff) +Blocked pending staging credentials/env: +- `API_BASE_URL` +- one of: `GOOGLE_ID_TOKEN` or `ACCESS_TOKEN` +- `STRIPE_TEST_PRICE_ID` (optional for billing endpoint verification; or configured `STRIPE_PRICE_PRO` on staging) +- `STRIPE_WEBHOOK_SECRET` + `STRIPE_CUSTOMER_ID` + `USER_ID` (for webhook transition script) + +- [ ] Google login works +- [ ] API key lifecycle works +- [ ] Game flow works (create->move->ai->resign) +- [ ] Clock timeout works +- [ ] Free-tier limits enforced +- [ ] Upgrade to pro updates limits +- [ ] Stripe webhook updates entitlements (ready to verify via `scripts/staging-webhook-verify.sh`) +- [ ] Docs examples execute successfully + +## Work Rules +- Keep scope strictly to SaaS chess API. +- No extra features unless directly required for reliability, billing, or developer onboarding. +- Commit in small, verifiable increments. diff --git a/Procfile b/Procfile deleted file mode 100644 index 063b78f..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: npm start diff --git a/README.md b/README.md index 1a95171..31482ff 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,188 @@ -# ♟️ Chess API ♟️ -API for playing chess and highscores storing. Api could be easily used to implement a multiplayer room based chess game, because every instance of a game initilizes new unique game_id. -* ### [API Documentation](https://documenter.getpostman.com/view/1741165/chess-api/7Lof2bk#intro) +# chess-api (NestJS rewrite) -## Feautures: +Proper modular architecture with NestJS: -* Player vs. Player game mode -* Player vs. Computer game mode -* Highscores data storing +- `AuthModule` (Google login + sessions) +- `UsersModule` (API key management) +- `ChessModule` (game lifecycle, clocks, PvP/PvE) +- `DatabaseModule` (Postgres + Drizzle) +- `HealthModule` (root/docs) +## Tech +- NestJS +- PostgreSQL +- Drizzle ORM +- Google OAuth ID token verification +- chess.js + chess-ai-kong -## Built With +## Env +```env +DATABASE_URL=postgres://... +GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com +STRIPE_SECRET_KEY=sk_test_... +STRIPE_WEBHOOK_SECRET=whsec_... +STRIPE_PRICE_PRO=price_... +APP_URL=http://localhost:3000 +PORT=3000 +``` -* [Chess.js](https://github.com/jhlywa/chess.js) -* [ChessCorp Artificial Intelligence: Kong](https://www.npmjs.com/package/chess-ai-kong) +## Run API +```bash +npm install +npm run dev +``` + +## Staging verification script +```bash +API_BASE_URL=https://stg-api.example.com \ +GOOGLE_ID_TOKEN= \ +bash scripts/staging-verify.sh +``` + +## QA checklist runner (staging) +```bash +API_BASE_URL=https://stg-api.example.com \ +GOOGLE_ID_TOKEN= \ +bash scripts/qa-checklist.sh +``` +(Or provide `ACCESS_TOKEN` directly to skip login bootstrap.) + +## Stripe webhook transition verifier (staging) +Use this after you have a real staged user + Stripe customer id. + +```bash +API_BASE_URL=https://stg-api.example.com \ +STRIPE_WEBHOOK_SECRET=whsec_xxx \ +STRIPE_CUSTOMER_ID=cus_xxx \ +USER_ID= \ +bash scripts/staging-webhook-verify.sh +``` + +Optional billing checks: +```bash +API_BASE_URL=https://stg-api.example.com \ +GOOGLE_ID_TOKEN= \ +STRIPE_TEST_PRICE_ID=price_xxx \ +bash scripts/qa-checklist.sh +``` + +## Landing page + docs + login + dashboard (shadcn-style UI) +A separate Next.js app lives in `web/` and is wired to the API. + +```bash +cd web +cp .env.example .env.local +npm install +npm run dev +``` + +### Vercel deploy (web) +`web/vercel.json` is included for Vercel deployment. + +Required Vercel project env vars: +- `NEXT_PUBLIC_API_BASE_URL` +- `NEXT_PUBLIC_GOOGLE_CLIENT_ID` + +Suggested project root in Vercel: `web/` + +## Auth routes +- `POST /auth/google` with `{ idToken }` +- `GET /me` (Bearer session token) +- `GET /me/api-keys` +- `POST /me/api-keys` +- `DELETE /me/api-keys/:id` + +## Chess routes (require `x-api-key`) +- `GET /games` +- `GET /games/players` +- `POST /games/players` +- `POST /games/:id/players` +- `POST /games` +- `GET /games/:id` +- `DELETE /games/:id` +- `GET /games/:id/moves` +- `POST /games/:id/moves` +- `POST /games/:id/ai-move` +- `POST /games/:id/resign` + +## Get key in 60 seconds +1. **Sign in** via `POST /auth/google` with your Google ID token. +2. **Save bearer token** from `accessToken` in response. +3. **Create API key** via `POST /me/api-keys` using bearer auth. +4. **Call chess API** with `x-api-key: `. + +Quick example: +```bash +# 1) Google login +curl -s -X POST http://localhost:3000/auth/google \ + -H "content-type: application/json" \ + -d '{"idToken":""}' + +# 2) Create API key (replace $ACCESS_TOKEN) +curl -s -X POST http://localhost:3000/me/api-keys \ + -H "authorization: Bearer $ACCESS_TOKEN" \ + -H "content-type: application/json" \ + -d '{"name":"quickstart"}' + +# 3) Create a game (replace $API_KEY) +curl -s -X POST http://localhost:3000/games \ + -H "x-api-key: $API_KEY" \ + -H "content-type: application/json" \ + -d '{"mode":"pve","aiColor":"b"}' +``` + +## Plan limits + upgrade path + +### Free +- API keys: **2 active keys** +- Rate limit: **30 requests/minute** per API key +- Monthly quota: **10,000 requests** per account + +### Pro +- API keys: **20 active keys** +- Rate limit: **300 requests/minute** per API key +- Monthly quota: **1,000,000 requests** per account + +### Upgrade +1. Create checkout session: `POST /billing/checkout-session` +2. Redirect user to returned Stripe URL +3. Stripe webhook updates plan to `pro` when subscription is active +4. User can manage billing via `POST /billing/portal-session` + +Minimal checkout call: +```bash +curl -s -X POST http://localhost:3000/billing/checkout-session \ + -H "authorization: Bearer $ACCESS_TOKEN" \ + -H "content-type: application/json" \ + -d '{}' +``` + +## Error code catalog +All API errors use a consistent shape: + +```json +{ + "error": { + "statusCode": 400, + "code": "Bad Request", + "message": "Human-readable message" + }, + "path": "/games", + "timestamp": "2026-02-22T22:00:00.000Z" +} +``` + +Common errors: +- `401 Unauthorized` + - Missing/invalid bearer session (`/me`, `/billing/*`) + - Missing/invalid `x-api-key` (`/games*`) +- `404 Not Found` + - Game or player not found / not owned by caller +- `429 Too Many Requests` + - API key RPM exceeded for current plan + - Monthly request quota exceeded for current plan +- `400 Bad Request` + - Validation failures (DTO constraints) + - Missing billing config (e.g. Stripe price) +- `500 Internal Server Error` + - Unhandled server-side failure diff --git a/api/controllers/chessOnePlayerController.js b/api/controllers/chessOnePlayerController.js deleted file mode 100644 index 2bb4434..0000000 --- a/api/controllers/chessOnePlayerController.js +++ /dev/null @@ -1,692 +0,0 @@ -'use strict'; - -var mongoose = require('mongoose'); -var chessAi = require('chess-ai-kong'); -var Chess = require('chess.js').Chess; -// var chess = null; - -var ChessGame = mongoose.model('Chess'); -var AIMoves = mongoose.model('AIMoves'); -var Moves = mongoose.model('Moves'); -var Status = mongoose.model('Status'); -var status = new Status(); - -// var movesArr = []; - - -exports.startNewGame = function(req, res) { - var chess = new Chess(); - var gameId = mongoose.Types.ObjectId(); - var chessGame = new ChessGame(); - chessGame.chess = chess.fen(); - chessGame.game_id = gameId; - - chessGame.save(function(err, chess) { - if(err) { - res.send(err); - } - var gameStatus = new Status(); - - gameStatus.status = "new game started"; - gameStatus.game_id = gameId; - - res.json(gameStatus); - }); - - // printChessboard(); - -}; - - -exports.startNewGameWithFEN = function(req, res) { - - var fenString = req.body.fen; - var chess = new Chess(); - var validation = chess.validate_fen(fenString); - - if(validation.valid) { - - chess.load(fenString); - var gameId = mongoose.Types.ObjectId(); - var chessGame = new ChessGame(); - chessGame.chess = chess.fen(); - chessGame.game_id = gameId; - - chessGame.save(function(err, chess) { - if(err) { - res.send(err); - } - var gameStatus = new Status(); - - gameStatus.status = "new game started from FEN"; - gameStatus.game_id = gameId; - - res.json(gameStatus); - }); - - // printChessboard(); - - } else { - status.status = "error: invalid FEN string!"; - res.json(status); - - } -}; - - -exports.startNewGameWithPgn = function(req, res) { - - var pgnString = req.body.pgn; - var chess = new Chess(); - var valid = chess.load_pgn(pgnString); - - if(valid) { - - chess.load_pgn(pgnString); - var gameId = mongoose.Types.ObjectId(); - var chessGame = new ChessGame(); - chessGame.chess = chess.fen(); - chessGame.game_id = gameId; - - chessGame.save(function(err, chess) { - if(err) { - res.send(err); - } - var gameStatus = new Status(); - - gameStatus.status = "new game started from pgn"; - gameStatus.game_id = gameId; - - res.json(gameStatus); - }); - - // printChessboard(); - - } else { - status.status = "error: invalid pgn string!"; - res.json(status); - - } - - -}; - -/** Params: a pgn position in json as {position: currentPosition} **/ -exports.listPosibleMoves = function(req, res) { - - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - if(currentGame != null){ - - var chess = new Chess(currentGame.chess); - - if(chess != null) { - - var sq = req.body.position; - var moves = new Moves(); - - var posibleMoves = chess.moves({square: sq}); - - for(var i = 0; i < posibleMoves.length; i++) { - if(posibleMoves[i].length > 2) { - var tmp = posibleMoves[i]; - while(tmp.length > 2) { - tmp = tmp.substring(1); - } - posibleMoves[i] = tmp; - } - } - - moves.moves = posibleMoves; - res.json(moves); - - } else { - status.status = "error: chess was not initialized!"; - res.json(status); - - } - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - - // printChessboard(chess); - }); - - - getChess(gameId, function(currentGame) { - // .... - }); -}; - -/** Params: from(pgn currentPosition) -> to(pgn desiredPosition) **/ -exports.move = function(req, res) { - - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var movesArr = currentGame.chess_moves; - - if(chess != null) { - var fromSq = req.body.from; - var toSq = req.body.to; - - var move = chess.move({ from: fromSq, to: toSq }); - - if(move != null) { - - movesArr.push(move.san); - ChessGame.update({ game_id: gameId }, - { - chess: chess.fen() , - chess_moves: movesArr - }, - - function (err, chess){ - if(err) { - res.send(err); - } - status.status = "figure moved"; - res.json(status); - }); - - // printChessboard(chess); - - } else { - status.status = "error: invalid move!"; - res.json(status); - } - - } else { - status.status = "error: chess was not initialized!"; - res.json(status); - - } - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - - -exports.checkGameOver = function(req, res) { - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - - if(chess != null) { - if(chess.game_over()) { - status.game_over_status = true; - - if(chess.in_checkmate()) { - status.status = "check mate"; - res.json(status); - console.log("Check mate!"); - - } else if (chess.in_draw()) { - status.status = "draw"; - res.json(status); - console.log("Draw!"); - - } else if (chess.in_stalemate()) { - status.status = "in stalemate"; - res.json(status); - - } else if (chess.in_threefold_repetition()) { - status.status = "in threefold repetition"; - res.json(status); - - } else if (chess.insufficient_material()) { - status.status = "insufficient material"; - res.json(status); - - } - - chess.clear(); - - } else { - status.status = "game continues" - res.json(status); - - } - - } else { - status.status = "error: chess was not initialized!"; - res.json(status); - - } - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - -exports.checkPosition = function(req, res) { - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var positionStatus = new Status(); - - positionStatus.position = chess.get(res.body.position) - res.json(positionStatus); - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - -exports.moveAI = function(req, res) { - - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - - var chess = new Chess(currentGame.chess); - var movesArr = currentGame.chess_moves; - - if(chess != null) { - var move = chessAi.play(movesArr); - var makeMove = chess.move(move); - - if(makeMove != null) { - - var from = makeMove.from; - var to = makeMove.to; - movesArr.push(makeMove.san); - - - ChessGame.update({ game_id: gameId }, - { - chess: chess.fen() , - chess_moves: movesArr - }, - - function (err, chess){ - if(err) { - res.send(err); - } - var aiMoves = new AIMoves(); - aiMoves.status = "AI moved!"; - aiMoves.to = to; - aiMoves.from = from; - - res.json(aiMoves); - }); - - printChessboard(chess); - - } else { - status.status = "error: invalid move!"; - res.json(status); - } - - } else { - status.status = "error: chess was not initialized!"; - res.json(status); - } - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - - }); -}; - - -exports.returnFEN = function(req, res){ - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var fenStatus = new Status(); - - fenStatus.fen_string = chess.fen(); - res.json(fenStatus); - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - - - -exports.returnAscii = function(req, res){ - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var asciiStatus = new Status(); - - asciiStatus.ascii = chess.ascii(); - res.json(asciiStatus); - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - -exports.returnPgn = function(req, res){ - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var pgnStatus = new Status(); - - pgnStatus.pgn = chess.pgn(); - res.json(pgnStatus); - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - -exports.undoLastMove = function(req, res){ - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var movesArr = currentGame.chess_moves; - var succes = chess.undo(); - console.log(succes); - if(succes != null) { - movesArr.pop(); - - ChessGame.update({ game_id: gameId }, - { - chess: chess.fen(), - chess_moves: movesArr - }, - - function (err, chess){ - if(err) { - res.send(err); - } - - status.status = "move undone"; - res.json(status); - }); - - - } else { - status.status = "error: couldn't undo the move!"; - res.json(status); - } - printChessboard(chess); - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - } - }); -}; - - - - -exports.resetBoard = function(req, res){ - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - chess.reset(); - - ChessGame.update({ game_id: gameId }, - { - chess: chess.fen() - }, - - function (err, chess){ - if(err) { - res.send(err); - } - - status.status = "board reseted"; - res.json(status); - }); - - printChessboard(chess); - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - - - - -exports.clearBoard = function(req, res){ - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - chess.clear(); - - ChessGame.update({ game_id: gameId }, - { - chess: chess.fen() - }, - - function (err, chess){ - if(err) { - res.send(err); - } - - status.status = "board cleared"; - res.json(status); - }); - - printChessboard(chess); - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - -exports.loadFenOverCurrent = function(req, res){ - var gameId = req.body.game_id; - var fenString = req.body.fen; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var validation = chess.validate_fen(fenString); - - if(validation.valid) { - - chess.load(fenString); - - ChessGame.update({ game_id: gameId }, - { - chess: chess.fen(), - chess_moves: [] - }, - - function (err, chess){ - if(err) { - res.send(err); - } - - status.status = "FEN loaded!"; - res.json(status); - }); - - // printChessboard(chess); - - } else { - status.status = "error: invalid FEN string!"; - res.json(status); - - } - - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - - -exports.loadPgnOverCurrent = function(req, res){ - var gameId = req.body.game_id; - var pgnString = req.body.pgn; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var valid = chess.load_pgn(pgnString); - - if(valid) { - - ChessGame.update({ game_id: gameId }, - { - chess: chess.fen(), - chess_moves: [] - }, - - function (err, chess){ - if(err) { - res.send(err); - } - - status.status = "Pgn loaded!"; - res.json(status); - }); - - // printChessboard(chess); - - } else { - status.status = "error: invalid pgn string!"; - res.json(status); - - } - - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - - - - - -exports.returnTurn = function(req, res){ - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var turnStatus = new Status(); - - turnStatus.turn = chess.turn(); - res.json(turnStatus); - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - - - - - - -function getChess(gameId, callback) { - ChessGame.findOne({ game_id: gameId }, function(err, chessGame) { - if(err) { - return null; - } - callback(chessGame); - }); -} - -// TODO : .get(square) -// .header() - - -// .put(piece, square) -// remove(square) - -// .square_color(square) - -// .validate_fen(fen): - - - - - -function printChessboard(chess) { - console.log(); - console.log("##########################################"); - console.log(); - console.log(chess.ascii()); - - var turn = "" - if(chess.turn() == "w") { - turn = "white"; - } else { - turn = "black"; - } - - console.log("Turn: " + turn ); -} diff --git a/api/controllers/chessTwoPlayersController.js b/api/controllers/chessTwoPlayersController.js deleted file mode 100644 index f39e68b..0000000 --- a/api/controllers/chessTwoPlayersController.js +++ /dev/null @@ -1,631 +0,0 @@ -'use strict'; - -var mongoose = require('mongoose'); -var chessAi = require('chess-ai-kong'); -var Chess = require('chess.js').Chess; -// var chess = null; - -var ChessGame = mongoose.model('Chess'); -var AIMoves = mongoose.model('AIMoves'); -var Moves = mongoose.model('Moves'); -var Status = mongoose.model('Status'); -var status = new Status(); - -// var movesArr = []; - - -exports.startNewGame = function(req, res) { - var chess = new Chess(); - var gameId = mongoose.Types.ObjectId(); - var chessGame = new ChessGame(); - chessGame.chess = chess.fen(); - chessGame.game_id = gameId; - - chessGame.save(function(err, chess) { - if(err) { - res.send(err); - } - var gameStatus = new Status(); - - gameStatus.status = "new game started"; - gameStatus.game_id = gameId; - - res.json(gameStatus); - }); - - // printChessboard(); - -}; - - -exports.startNewGameWithFEN = function(req, res) { - - var fenString = req.body.fen; - var chess = new Chess(); - var validation = chess.validate_fen(fenString); - - if(validation.valid) { - - chess.load(fenString); - var gameId = mongoose.Types.ObjectId(); - var chessGame = new ChessGame(); - chessGame.chess = chess.fen(); - chessGame.game_id = gameId; - - chessGame.save(function(err, chess) { - if(err) { - res.send(err); - } - var gameStatus = new Status(); - - gameStatus.status = "new game started from FEN"; - gameStatus.game_id = gameId; - - res.json(gameStatus); - }); - - // printChessboard(); - - } else { - status.status = "error: invalid FEN string!"; - res.json(status); - - } -}; - - -exports.startNewGameWithPgn = function(req, res) { - - var pgnString = req.body.pgn; - var chess = new Chess(); - var valid = chess.load_pgn(pgnString); - - if(valid) { - - chess.load_pgn(pgnString); - var gameId = mongoose.Types.ObjectId(); - var chessGame = new ChessGame(); - chessGame.chess = chess.fen(); - chessGame.game_id = gameId; - - chessGame.save(function(err, chess) { - if(err) { - res.send(err); - } - var gameStatus = new Status(); - - gameStatus.status = "new game started from pgn"; - gameStatus.game_id = gameId; - - res.json(gameStatus); - }); - - // printChessboard(); - - } else { - status.status = "error: invalid pgn string!"; - res.json(status); - - } - - -}; - -/** Params: a pgn position in json as {position: currentPosition} **/ -exports.listPosibleMoves = function(req, res) { - - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - if(currentGame != null){ - - var chess = new Chess(currentGame.chess); - - if(chess != null) { - - var sq = req.body.position; - var moves = new Moves(); - - var posibleMoves = chess.moves({square: sq}); - - for(var i = 0; i < posibleMoves.length; i++) { - if(posibleMoves[i].length > 2) { - var tmp = posibleMoves[i]; - while(tmp.length > 2) { - tmp = tmp.substring(1); - } - posibleMoves[i] = tmp; - } - } - - moves.moves = posibleMoves; - res.json(moves); - - } else { - status.status = "error: chess was not initialized!"; - res.json(status); - - } - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - - // printChessboard(chess); - }); - - - getChess(gameId, function(currentGame) { - // .... - }); -}; - -/** Params: from(pgn currentPosition) -> to(pgn desiredPosition) **/ -exports.move = function(req, res) { - - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var movesArr = currentGame.chess_moves; - - if(chess != null) { - var fromSq = req.body.from; - var toSq = req.body.to; - - var move = chess.move({ from: fromSq, to: toSq }); - - if(move != null) { - - movesArr.push(move.san); - ChessGame.update({ game_id: gameId }, - { - chess: chess.fen() , - chess_moves: movesArr - }, - - function (err, chess){ - if(err) { - res.send(err); - } - status.status = "figure moved"; - res.json(status); - }); - - // printChessboard(chess); - - } else { - status.status = "error: invalid move!"; - res.json(status); - } - - } else { - status.status = "error: chess was not initialized!"; - res.json(status); - - } - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - - -exports.checkGameOver = function(req, res) { - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - - if(chess != null) { - if(chess.game_over()) { - status.game_over_status = true; - - if(chess.in_checkmate()) { - status.status = "check mate"; - res.json(status); - console.log("Check mate!"); - - } else if (chess.in_draw()) { - status.status = "draw"; - res.json(status); - console.log("Draw!"); - - } else if (chess.in_stalemate()) { - status.status = "in stalemate"; - res.json(status); - - } else if (chess.in_threefold_repetition()) { - status.status = "in threefold repetition"; - res.json(status); - - } else if (chess.insufficient_material()) { - status.status = "insufficient material"; - res.json(status); - - } - - chess.clear(); - - } else { - status.status = "game continues" - res.json(status); - - } - - } else { - status.status = "error: chess was not initialized!"; - res.json(status); - - } - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - - -exports.checkPosition = function(req, res) { - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var positionStatus = new Status(); - - positionStatus.position = chess.get(res.body.position) - res.json(positionStatus); - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - -exports.returnFEN = function(req, res){ - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var fenStatus = new Status(); - - fenStatus.fen_string = chess.fen(); - res.json(fenStatus); - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - - - -exports.returnAscii = function(req, res){ - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var asciiStatus = new Status(); - - asciiStatus.ascii = chess.ascii(); - res.json(asciiStatus); - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - -exports.returnPgn = function(req, res){ - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var pgnStatus = new Status(); - - pgnStatus.pgn = chess.pgn(); - res.json(pgnStatus); - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - -exports.undoLastMove = function(req, res){ - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var movesArr = currentGame.chess_moves; - var succes = chess.undo(); - if(succes != null) { - movesArr.pop(); - - ChessGame.update({ game_id: gameId }, - { - chess: chess.fen(), - chess_moves: movesArr - }, - - function (err, chess){ - if(err) { - res.send(err); - } - - status.status = "move undone"; - res.json(status); - }); - - - } else { - status.status = "error: couldn't undo the move!"; - res.json(status); - } - printChessboard(chess); - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - } - }); -}; - - - - -exports.resetBoard = function(req, res){ - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - chess.reset(); - - ChessGame.update({ game_id: gameId }, - { - chess: chess.fen() - }, - - function (err, chess){ - if(err) { - res.send(err); - } - - status.status = "board reseted"; - res.json(status); - }); - - printChessboard(chess); - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - - - - -exports.clearBoard = function(req, res){ - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - chess.clear(); - - ChessGame.update({ game_id: gameId }, - { - chess: chess.fen() - }, - - function (err, chess){ - if(err) { - res.send(err); - } - - status.status = "board cleared"; - res.json(status); - }); - - printChessboard(chess); - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - -exports.loadFenOverCurrent = function(req, res){ - var gameId = req.body.game_id; - var fenString = req.body.fen; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var validation = chess.validate_fen(fenString); - - if(validation.valid) { - - chess.load(fenString); - - ChessGame.update({ game_id: gameId }, - { - chess: chess.fen(), - chess_moves: [] - }, - - function (err, chess){ - if(err) { - res.send(err); - } - - status.status = "FEN loaded!"; - res.json(status); - }); - - // printChessboard(chess); - - } else { - status.status = "error: invalid FEN string!"; - res.json(status); - - } - - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - - -exports.loadPgnOverCurrent = function(req, res){ - var gameId = req.body.game_id; - var pgnString = req.body.pgn; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var valid = chess.load_pgn(pgnString); - - if(valid) { - - ChessGame.update({ game_id: gameId }, - { - chess: chess.fen(), - chess_moves: [] - }, - - function (err, chess){ - if(err) { - res.send(err); - } - - status.status = "Pgn loaded!"; - res.json(status); - }); - - // printChessboard(chess); - - } else { - status.status = "error: invalid pgn string!"; - res.json(status); - - } - - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - - - - - -exports.returnTurn = function(req, res){ - var gameId = req.body.game_id; - - getChess(gameId, currentGame => { - - if(currentGame != null) { - var chess = new Chess(currentGame.chess); - var turnStatus = new Status(); - - turnStatus.turn = chess.turn(); - res.json(turnStatus); - - } else { - status.status = "error: The game has expired OR you didn't put the game_id as the parameter!"; - res.json(status); - - } - }); -}; - - - - - - -function getChess(gameId, callback) { - ChessGame.findOne({ game_id: gameId }, function(err, chessGame) { - if(err) { - return null; - } - callback(chessGame); - }); -} - -// TODO : .get(square) -// .header() - - -// .put(piece, square) -// remove(square) - -// .square_color(square) - -// .validate_fen(fen): - - - - - -function printChessboard(chess) { - console.log(); - console.log("##########################################"); - console.log(); - console.log(chess.ascii()); - - var turn = "" - if(chess.turn() == "w") { - turn = "white"; - } else { - turn = "black"; - } - - console.log("Turn: " + turn ); -} diff --git a/api/controllers/highScoresController.js b/api/controllers/highScoresController.js deleted file mode 100644 index f6c23fe..0000000 --- a/api/controllers/highScoresController.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -var hhmmss = require('hhmmss'); -var mongoose = require('mongoose'); -var Player = mongoose.model('Player'); -var Status = mongoose.model('Status'); - -var status = new Status(); - -exports.createNewLocalScoreboard = function(req, res) { - var scoreBoardId = mongoose.Types.ObjectId(); - - status.status = "Scoreboard created!"; - status.scoreboard_id = scoreBoardId; - - res.json(status); -}; - - - -exports.addNewPlayer = function(req, res) { - var newPlayer = new Player(req.body); - - newPlayer.save(function(err, player) { - if(err) { - res.send(err); - } - res.json(player); - }); -}; - - - -exports.lisTopHighScores = function(req, res) { - var scoreBoardId = req.body.scoreboard_id; - console.log(scoreBoardId);; - Player.find( - { scoreboard_id: scoreBoardId }, //filter by scoreboard ID - ['name', 'score', 'score_out', 'date', 'date_out'], //Return name, score and date - { - skip: 0, //Start at idx 0 - limit: 5, //finish at idx 5 - sort: { - score: 'asc' //Sort by score, ascending - } - } - - , function(err, topN) { - if(err) res.send(err); - - for(var i in topN) { - var date = ((topN[i].date + '').split('-') + '').split(" "); - topN[i].date_out = date[2] + " " + date[1] + " " + date[3]; - topN[i].score_out = hhmmss(topN[i].score); - } - - res.json(topN); - }); -}; diff --git a/api/models/chessboardModel.js b/api/models/chessboardModel.js deleted file mode 100644 index b3504de..0000000 --- a/api/models/chessboardModel.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -var mongoose = require('mongoose'); - -var StatusSchema = new mongoose.Schema({ - status: { - type: String - }, - - game_over_status: { - type: Boolean - }, - - game_id: { - type: String - }, - - fen_string: { - type: String - - }, - - turn: { - type: String - - }, - - ascii: { - type: String - - }, - - pgn: { - type: String - - } - - -}); - -module.exports = mongoose.model('Status', StatusSchema); - - - -var PossibleMovesSchema = new mongoose.Schema({ - moves: [{ - type: String - }] - -}); - -module.exports = mongoose.model('Moves', PossibleMovesSchema); - -var AIMovesSchema = new mongoose.Schema ({ - from: { - type: String - }, - - to: { - type: String - - }, - - status: { - type: String - - } - -}); - - -module.exports = mongoose.model('AIMoves', AIMovesSchema); - - -var ChessSchema = new mongoose.Schema ({ - chess: { - type: String - }, - - chess_moves: [{ - type: String - }], - - game_id: { - type: String - }, - - createAt: { - type: Date, - default: Date.now(), - index: { expires: '2h' }, - expireAfterSeconds: 7200 - } - - -}); - - -module.exports = mongoose.model('Chess', ChessSchema); diff --git a/api/models/db.js b/api/models/db.js deleted file mode 100644 index 560d315..0000000 --- a/api/models/db.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -var mongoose = require('mongoose'); -mongoose.Promise = global.Promise; - -var failedConnections = 0; -var autoReconnect = true; - -//var db_URI = 'mongodb://localhost/ElmChessDb' -var db_URI = 'mongodb://chess_player:chess_player@ds163016.mlab.com:63016/chess-highscores'; -connect(); - -function connect() { - mongoose.connect(db_URI, { useMongoClient: true }); -} - -/** Mongoose is connected **/ -mongoose.connection.on('connected', function() { - console.log('Mongoose database is connected on: ' + db_URI); - -}); - -/** Mongoose is disconnected--> Tries to reconnect three times, then gives up **/ -mongoose.connection.on('disconnected', function() { - console.log('Mongoose is disconnected.'); - if(failedConnections < 3) { - console.log('Trying to reconnect.. '); - connect(); - failedConnections++; - } -}); - -/** Mongoose error **/ -mongoose.connection.on('error', function(err) { - console.log('Mongoose encountered an error: ' + err); -}); - -/** Application closing **/ -process.on('SIGINT', function () { - console.log('Goodbye from mongoose! :)'); - process.exit(0); -}); - -/** Handles SIGUSR2 when nodemon restart **/ -process.once('SIGUSR2', function() { - console.log('Restarting mongoose.'); - process.kill(process.pid, 'SIGUSR2'); - -}); - - -/** Handles SIGUTERM after Heroku restar **/ -process.on('SIGTERM', function() { - console.log('Goodbye from Heroku! :)'); - process.exit(0); - -}); diff --git a/api/models/playerModel.js b/api/models/playerModel.js deleted file mode 100644 index e49c381..0000000 --- a/api/models/playerModel.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -var mongoose = require('mongoose'); - -var PlayerSchema = new mongoose.Schema ({ - name: { - type: String , - required:'Players name was not entered!' - }, - score: { - type: Number - }, - score_out: { - type: String - }, - date: { - type: Date, - default: Date.now - }, - date_out: { - type: String - }, - scoreboard_id: { - type: String - - } - - - -}); - -module.exports = mongoose.model('Player', PlayerSchema); diff --git a/api/routes/apiRoutes.js b/api/routes/apiRoutes.js deleted file mode 100644 index 9ef16e6..0000000 --- a/api/routes/apiRoutes.js +++ /dev/null @@ -1,137 +0,0 @@ -'use strict'; - -var express = require('express'); - -module.exports = function(app) { - var highScoresController = require('../controllers/highScoresController'); - var chessOnePlayerController = require('../controllers/chessOnePlayerController'); - var chessTwoPlayersController = require('../controllers/chessTwoPlayersController'); - - var apiRouter = express.Router(); - var versionRouter = express.Router(); - - app.use('/api', apiRouter); - apiRouter.use("/v1", versionRouter); - - - /** Chess routes **/ - - /** Player vs. Computer game mode **/ - versionRouter.route('/chess/one') - .get(chessOnePlayerController.startNewGame); - - versionRouter.route('/chess/one/moves') - .post(chessOnePlayerController.listPosibleMoves); - - versionRouter.route('/chess/one/move/player') - .post(chessOnePlayerController.move); - - versionRouter.route('/chess/one/move/ai') - .post(chessOnePlayerController.moveAI); - - versionRouter.route('/chess/one/check') - .post(chessOnePlayerController.checkGameOver); - - versionRouter.route('/chess/one/check-position') - .post(chessOnePlayerController.checkPosition); - - versionRouter.route('/chess/one/fen') - .post(chessOnePlayerController.returnFEN); - - versionRouter.route('/chess/one/turn') - .post(chessOnePlayerController.returnTurn); - - versionRouter.route('/chess/one/ascii') - .post(chessOnePlayerController.returnAscii); - - versionRouter.route('/chess/one/pgn') - .post(chessOnePlayerController.returnPgn); - - versionRouter.route('/chess/one/load/over/fen') - .post(chessOnePlayerController.loadFenOverCurrent); - - versionRouter.route('/chess/one/load/over/pgn') - .post(chessOnePlayerController.loadPgnOverCurrent); - - versionRouter.route('/chess/one/clear') - .post(chessOnePlayerController.clearBoard); - - versionRouter.route('/chess/one/reset') - .post(chessOnePlayerController.resetBoard); - - versionRouter.route('/chess/one/undo') - .post(chessOnePlayerController.undoLastMove); - - versionRouter.route('/chess/one/start/fen') - .post(chessOnePlayerController.startNewGameWithFEN); - - versionRouter.route('/chess/one/start/pgn') - .post(chessOnePlayerController.startNewGameWithFEN); - - - - - - - /** Player vs. Player game mode **/ - versionRouter.route('/chess/two') - .get(chessTwoPlayersController.startNewGame); - - versionRouter.route('/chess/two/moves') - .post(chessTwoPlayersController.listPosibleMoves); - - versionRouter.route('/chess/two/move') - .post(chessTwoPlayersController.move); - - versionRouter.route('/chess/two/check') - .post(chessTwoPlayersController.checkGameOver); - - versionRouter.route('/chess/two/check-position') - .post(chessTwoPlayersController.checkPosition); - - versionRouter.route('/chess/two/fen') - .post(chessTwoPlayersController.returnFEN); - - versionRouter.route('/chess/two/turn') - .post(chessTwoPlayersController.returnTurn); - - versionRouter.route('/chess/two/ascii') - .post(chessTwoPlayersController.returnAscii); - - versionRouter.route('/chess/two/pgn') - .post(chessTwoPlayersController.returnPgn); - - versionRouter.route('/chess/two/load/over/fen') - .post(chessTwoPlayersController.loadFenOverCurrent); - - versionRouter.route('/chess/two/load/over/pgn') - .post(chessTwoPlayersController.loadPgnOverCurrent); - - versionRouter.route('/chess/two/clear') - .post(chessTwoPlayersController.clearBoard); - - versionRouter.route('/chess/two/reset') - .post(chessTwoPlayersController.resetBoard); - - versionRouter.route('/chess/two/undo') - .post(chessTwoPlayersController.undoLastMove); - - versionRouter.route('/chess/two/start/fen') - .post(chessTwoPlayersController.startNewGameWithFEN); - - versionRouter.route('/chess/two/start/pgn') - .post(chessTwoPlayersController.startNewGameWithFEN); - - - /** Highscores routes **/ - - versionRouter.route('/scoreboard') - .get(highScoresController.createNewLocalScoreboard); - - versionRouter.route('/highscores') - .post(highScoresController.lisTopHighScores); - - versionRouter.route('/highscores/add') - .post(highScoresController.addNewPlayer); - -}; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0c8bdee --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.9' +services: + postgres: + image: postgres:16 + restart: unless-stopped + environment: + POSTGRES_USER: chess + POSTGRES_PASSWORD: chess + POSTGRES_DB: chess_api + ports: + - '5432:5432' + volumes: + - pgdata:/var/lib/postgresql/data +volumes: + pgdata: diff --git a/index.js b/index.js deleted file mode 100644 index 813e91d..0000000 --- a/index.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -var express = require('express'); -var bodyParser = require('body-parser'); -var dataBase = require('./api/models/db'); -var routesApi = require('./api/routes/apiRoutes'); -var app = express(); -var player = require('./api/models/playerModel'); -var status = require('./api/models/chessboardModel'); -var port = process.env.PORT || 3000; - - -// var server = app.listen(3000, function () { -// var host = server.address().address; -// host = (host === '::' ? 'localhost' : host); -// var port = server.address().port; -// -// console.log('Listening at: http://%s:%s', host, port); -// -// }); - - - - -app.get('/', function (req, res) { - res.send('

Up and running.

'); -}); - -app.use(bodyParser.urlencoded({ extended: true })); -app.use(bodyParser.json()); - -routesApi(app); - -app.listen(port); -console.log("Up and running on port: " + port); - - - - - - - -/** Error handling **/ - -app.use(function(req, res) { - res.status(404).send({error: 'Url not found!', url: req.originalUrl}) - -}); diff --git a/package-lock.json b/package-lock.json index 7625b27..4cb2ebd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,1206 +1,2615 @@ { - "name": "elm_chess_api", - "version": "1.0.0", - "lockfileVersion": 1, + "name": "chess-api", + "version": "2.0.0-alpha.1", + "lockfileVersion": 3, "requires": true, - "dependencies": { - "accepts": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", - "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", - "requires": { - "mime-types": "2.1.17", - "negotiator": "0.6.1" - } - }, - "append-field": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-0.1.0.tgz", - "integrity": "sha1-bdxY+gg8e8VF08WZWygwzCNm1Eo=" - }, - "argparse": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", - "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", - "requires": { - "sprintf-js": "1.0.3" - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" - }, - "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", - "requires": { - "bytes": "3.0.0", - "content-type": "1.0.4", - "debug": "2.6.9", - "depd": "1.1.1", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", - "on-finished": "2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "1.6.15" + "packages": { + "": { + "name": "chess-api", + "version": "2.0.0-alpha.1", + "dependencies": { + "@fastify/cors": "^10.0.1", + "@fastify/rate-limit": "^10.1.0", + "@nestjs/common": "^10.4.2", + "@nestjs/config": "^3.2.3", + "@nestjs/core": "^10.4.2", + "@nestjs/platform-express": "^10.4.2", + "chess-ai-kong": "^0.3.3", + "chess.js": "^1.4.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "drizzle-orm": "^0.39.0", + "google-auth-library": "^9.15.1", + "postgres": "^3.4.5", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "stripe": "^20.3.1", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz", + "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/cors": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-10.1.0.tgz", + "integrity": "sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "mnemonist": "0.40.0" + } + }, + "node_modules/@fastify/rate-limit": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-10.3.0.tgz", + "integrity": "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "fastify-plugin": "^5.0.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/common": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.22.tgz", + "integrity": "sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw==", + "license": "MIT", + "dependencies": { + "file-type": "20.4.1", + "iterare": "1.2.1", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.3.0.tgz", + "integrity": "sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.5", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" } }, - "bson": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", - "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" + "node_modules/@nestjs/core": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.22.tgz", + "integrity": "sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-express": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.22.tgz", + "integrity": "sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==", + "license": "MIT", + "dependencies": { + "body-parser": "1.20.4", + "cors": "2.8.5", + "express": "4.22.1", + "multer": "2.0.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + } + }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } }, - "buffer-shims": { + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/append-field": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", - "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" - }, - "busboy": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", - "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", - "requires": { - "dicer": "0.2.5", - "readable-stream": "1.1.14" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + { + "type": "consulting", + "url": "https://feross.org/support" } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" } }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "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.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "chess-ai-kong": { + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chess-ai-kong": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/chess-ai-kong/-/chess-ai-kong-0.3.3.tgz", - "integrity": "sha1-hN4uF9tAnwWboijBHVDI5MjvN7I=", - "requires": { - "chess-rules": "0.10.2" + "integrity": "sha512-JuPK+AniBmacnJXYq5iZzKEmytqBwtJ8wITp1xnJ3TKzLknUQwT0isK0YXrCPpBhvk5Lh1YClMr5D09cDk9t1A==", + "license": "MIT", + "dependencies": { + "chess-rules": "^0.10.2" } }, - "chess-rules": { + "node_modules/chess-rules": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/chess-rules/-/chess-rules-0.10.2.tgz", - "integrity": "sha1-Kfrn8O6Gu3EB85YcL4Jv3G28ikI=", - "requires": { - "colors": "1.1.2", - "underscore-plus": "1.6.6" + "integrity": "sha512-S6x6Q78bKG5GTnollkBJ7GTAaUrySbmIxHdrkiaI6PfFWZFOpI2ZS11Qc1NYDdOPUqzwbjErEm3Ti3UJjdrNOA==", + "license": "MIT", + "dependencies": { + "colors": "^1.1.2", + "underscore-plus": "^1.6.6" } }, - "chess.js": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/chess.js/-/chess.js-0.10.2.tgz", - "integrity": "sha1-6s76IGcVX4fwRugMwS0gGe7xuBo=" + "node_modules/chess.js": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/chess.js/-/chess.js-1.4.0.tgz", + "integrity": "sha512-BBJgrrtKQOzFLonR0l+k64A98NLemPwNsCskwb+29bRwobUa4iTm51E1kwGPbWXAcfdDa18nad6vpPPKPWarqw==", + "license": "BSD-2-Clause" }, - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=" + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", + "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.20" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } }, - "combined-stream": { + "node_modules/content-type": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "requires": { - "delayed-stream": "1.0.0" + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "commander": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", - "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==" + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } }, - "concat-stream": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", - "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", - "requires": { - "inherits": "2.0.3", - "readable-stream": "2.2.7", - "typedarray": "0.0.6" + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + "node_modules/drizzle-orm": { + "version": "0.39.3", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.39.3.tgz", + "integrity": "sha512-EZ8ZpYvDIvKU9C56JYLOmUskazhad+uXZCTCRN4OnRMsL+xAJ05dv1eCpAG5xzhsm1hqiuC5kAZUCS924u2DTw==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } }, - "cookiejar": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.1.tgz", - "integrity": "sha1-Qa1XsbVVlR7BcUEqgZQrHoIA00o=" + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "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.7.1", + "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.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "debug": { + "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { + "license": "MIT", + "dependencies": { "ms": "2.0.0" } }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "dicer": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", - "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", - "requires": { - "readable-stream": "1.1.14", - "streamsearch": "0.1.2" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fastify-plugin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" } + ], + "license": "MIT" + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "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.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "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" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "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" } }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } }, - "encodeurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", - "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } }, - "esprima": { + "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "express": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", - "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", - "requires": { - "accepts": "1.3.4", - "array-flatten": "1.1.1", - "body-parser": "1.18.2", - "content-disposition": "0.5.2", - "content-type": "1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "1.1.1", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "etag": "1.8.1", - "finalhandler": "1.1.0", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "1.1.2", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "2.0.2", - "qs": "6.5.1", - "range-parser": "1.2.0", - "safe-buffer": "5.1.1", - "send": "0.16.1", - "serve-static": "1.13.1", - "setprototypeof": "1.1.0", - "statuses": "1.3.1", - "type-is": "1.6.15", - "utils-merge": "1.0.1", - "vary": "1.1.2" + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" } }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "finalhandler": { + "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", - "requires": { - "debug": "2.6.9", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "statuses": "1.3.1", - "unpipe": "1.0.0" + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" } }, - "formidable": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.1.1.tgz", - "integrity": "sha1-lriIb3w8NQi5Mta9cMTTqI818ak=" - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } }, - "graphlib": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.1.tgz", - "integrity": "sha1-QjUsUrovTQNctWbrkfc5X3bryVE=", - "requires": { - "lodash": "4.17.4" + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, - "hhmmss": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hhmmss/-/hhmmss-1.0.0.tgz", - "integrity": "sha1-BsdlqZCKiIS3IAPBeoOch5ypKnw=" - }, - "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "requires": { - "depd": "1.1.1", - "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": "1.3.1" - }, - "dependencies": { - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ipaddr.js": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", - "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } }, - "isarray": { + "node_modules/json-bigint": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "js-yaml": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", - "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", - "requires": { - "argparse": "1.0.9", - "esprima": "4.0.0" + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" } }, - "json-refs": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/json-refs/-/json-refs-3.0.2.tgz", - "integrity": "sha512-5SVFpK29GMOP1Ms6vS4JC18WA8Tzauzd+pVSXZZD90JgX9VrDW/vhO327r0L8kKmZvfDGEF8mGq5YqzbzXx1Kg==", - "requires": { - "commander": "2.12.2", - "graphlib": "2.1.1", - "js-yaml": "3.10.0", - "lodash": "4.17.4", - "native-promise-only": "0.8.1", - "path-loader": "1.0.4", - "slash": "1.0.0", - "uri-js": "3.0.2" - } - }, - "kareem": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.1.tgz", - "integrity": "sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw==" - }, - "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" - }, - "lodash._arraypool": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._arraypool/-/lodash._arraypool-2.4.1.tgz", - "integrity": "sha1-6I7suS4ruEyQZWEv2VigcZzUf5Q=" - }, - "lodash._basebind": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._basebind/-/lodash._basebind-2.4.1.tgz", - "integrity": "sha1-6UC5690nwyfgqNqxtVkWxTQelXU=", - "requires": { - "lodash._basecreate": "2.4.1", - "lodash._setbinddata": "2.4.1", - "lodash._slice": "2.4.1", - "lodash.isobject": "2.4.1" + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" } }, - "lodash._baseclone": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._baseclone/-/lodash._baseclone-2.4.1.tgz", - "integrity": "sha1-MPgj5X4X43NdODvWK2Czh1Q7QYY=", - "requires": { - "lodash._getarray": "2.4.1", - "lodash._releasearray": "2.4.1", - "lodash._slice": "2.4.1", - "lodash.assign": "2.4.1", - "lodash.foreach": "2.4.1", - "lodash.forown": "2.4.1", - "lodash.isarray": "2.4.1", - "lodash.isobject": "2.4.1" - } - }, - "lodash._basecreate": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-2.4.1.tgz", - "integrity": "sha1-+Ob1tXip405UEXm1a47uv0oofgg=", - "requires": { - "lodash._isnative": "2.4.1", - "lodash.isobject": "2.4.1", - "lodash.noop": "2.4.1" + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" } }, - "lodash._basecreatecallback": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._basecreatecallback/-/lodash._basecreatecallback-2.4.1.tgz", - "integrity": "sha1-fQsmdknLKeehOdAQO3wR+uhOSFE=", - "requires": { - "lodash._setbinddata": "2.4.1", - "lodash.bind": "2.4.1", - "lodash.identity": "2.4.1", - "lodash.support": "2.4.1" + "node_modules/libphonenumber-js": { + "version": "1.12.37", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.37.tgz", + "integrity": "sha512-rDU6bkpuMs8YRt/UpkuYEAsYSoNuDEbrE41I3KNvmXREGH6DGBJ8Wbak4by29wNOQ27zk4g4HL82zf0OGhwRuw==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" } }, - "lodash._basecreatewrapper": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._basecreatewrapper/-/lodash._basecreatewrapper-2.4.1.tgz", - "integrity": "sha1-TTHy595+E0+/KAN2K4FQsyUZZm8=", - "requires": { - "lodash._basecreate": "2.4.1", - "lodash._setbinddata": "2.4.1", - "lodash._slice": "2.4.1", - "lodash.isobject": "2.4.1" + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "lodash._createwrapper": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._createwrapper/-/lodash._createwrapper-2.4.1.tgz", - "integrity": "sha1-UdaVeXPaTtVW43KQ2MGhjFPeFgc=", - "requires": { - "lodash._basebind": "2.4.1", - "lodash._basecreatewrapper": "2.4.1", - "lodash._slice": "2.4.1", - "lodash.isfunction": "2.4.1" + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "lodash._getarray": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._getarray/-/lodash._getarray-2.4.1.tgz", - "integrity": "sha1-+vH3+BD6mFolHCGHQESBCUg55e4=", - "requires": { - "lodash._arraypool": "2.4.1" + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "lodash._isnative": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._isnative/-/lodash._isnative-2.4.1.tgz", - "integrity": "sha1-PqZAS3hKe+g2x7V1gOHN95sUgyw=" + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } }, - "lodash._maxpoolsize": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._maxpoolsize/-/lodash._maxpoolsize-2.4.1.tgz", - "integrity": "sha1-nUgvRjuOZq++WcLBTtsRcGAXIzQ=" + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "lodash._objecttypes": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", - "integrity": "sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE=" + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } }, - "lodash._releasearray": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._releasearray/-/lodash._releasearray-2.4.1.tgz", - "integrity": "sha1-phOWMNdtFTawfdyAliiJsIL2pkE=", - "requires": { - "lodash._arraypool": "2.4.1", - "lodash._maxpoolsize": "2.4.1" + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "lodash._setbinddata": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._setbinddata/-/lodash._setbinddata-2.4.1.tgz", - "integrity": "sha1-98IAzRuS7yNrOZ7s9zxkjReqlNI=", - "requires": { - "lodash._isnative": "2.4.1", - "lodash.noop": "2.4.1" + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" } }, - "lodash._shimkeys": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._shimkeys/-/lodash._shimkeys-2.4.1.tgz", - "integrity": "sha1-bpzJZm/wgfC1psl4uD4kLmlJ0gM=", - "requires": { - "lodash._objecttypes": "2.4.1" + "node_modules/mnemonist": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.0.tgz", + "integrity": "sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.4" } }, - "lodash._slice": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._slice/-/lodash._slice-2.4.1.tgz", - "integrity": "sha1-dFz0GlNZexj2iImFREBe+isG2Q8=" + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, - "lodash.assign": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-2.4.1.tgz", - "integrity": "sha1-hMOVlt1xGBqXsGUpE6fJZ15Jsao=", - "requires": { - "lodash._basecreatecallback": "2.4.1", - "lodash._objecttypes": "2.4.1", - "lodash.keys": "2.4.1" + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" } }, - "lodash.bind": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-2.4.1.tgz", - "integrity": "sha1-XRn6AFyMTSNvr0dCx7eh/Kvikmc=", - "requires": { - "lodash._createwrapper": "2.4.1", - "lodash._slice": "2.4.1" + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "lodash.clonedeep": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-2.4.1.tgz", - "integrity": "sha1-8pIDtAsS/uCkXTYxZIJZvrq8eGg=", - "requires": { - "lodash._baseclone": "2.4.1", - "lodash._basecreatecallback": "2.4.1" + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "lodash.foreach": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-2.4.1.tgz", - "integrity": "sha1-/j/Do0yGyUyrb5UiVgKCdB4BYwk=", - "requires": { - "lodash._basecreatecallback": "2.4.1", - "lodash.forown": "2.4.1" + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "lodash.forown": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.forown/-/lodash.forown-2.4.1.tgz", - "integrity": "sha1-eLQer+FAX6lmRZ6kGT/VAtCEUks=", - "requires": { - "lodash._basecreatecallback": "2.4.1", - "lodash._objecttypes": "2.4.1", - "lodash.keys": "2.4.1" + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" }, - "lodash.identity": { + "node_modules/on-finished": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.identity/-/lodash.identity-2.4.1.tgz", - "integrity": "sha1-ZpTP+mX++TH3wxzobHRZfPVg9PE=" - }, - "lodash.isarray": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-2.4.1.tgz", - "integrity": "sha1-tSoybB9i9tfac6MdVAHfbvRPD6E=", - "requires": { - "lodash._isnative": "2.4.1" + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" } }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" - }, - "lodash.isfunction": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-2.4.1.tgz", - "integrity": "sha1-LP1XXHPkmKtX4xm3f6Aq3vE6lNE=" - }, - "lodash.isobject": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", - "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", - "requires": { - "lodash._objecttypes": "2.4.1" + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, - "lodash.keys": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", - "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", - "requires": { - "lodash._isnative": "2.4.1", - "lodash._shimkeys": "2.4.1", - "lodash.isobject": "2.4.1" + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/postgres": { + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.8.tgz", + "integrity": "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" } }, - "lodash.noop": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-2.4.1.tgz", - "integrity": "sha1-T7VPgWZS5a4Q6PcvcXo4jHMmU4o=" + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } }, - "lodash.support": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.support/-/lodash.support-2.4.1.tgz", - "integrity": "sha1-Mg4LZwMWc8KNeiu12eAzGkUkBRU=", - "requires": { - "lodash._isnative": "2.4.1" + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" } }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } }, - "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" }, - "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "requires": { - "mime-db": "1.30.0" + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "mongodb": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.3.2.tgz", - "integrity": "sha512-fqJt3iywelk4yKu/lfwQg163Bjpo5zDKhXiohycvon4iQHbrfflSAz9AIlRE6496Pm/dQKQK5bMigdVo2s6gBg==", - "requires": { - "bson": "^1.1.1", - "require_optional": "^1.0.1", - "safe-buffer": "^5.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - } + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" } }, - "mongoose": { - "version": "5.7.5", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.7.5.tgz", - "integrity": "sha512-BZ4FxtnbTurc/wcm/hLltLdI4IDxo4nsE0D9q58YymTdZwreNzwO62CcjVtaHhmr8HmJtOInp2W/T12FZaMf8g==", - "requires": { - "bson": "~1.1.1", - "kareem": "2.3.1", - "mongodb": "3.3.2", - "mongoose-legacy-pluralize": "1.0.2", - "mpath": "0.6.0", - "mquery": "3.2.2", - "ms": "2.1.2", - "regexp-clone": "1.0.0", - "safe-buffer": "5.1.2", - "sift": "7.0.1", - "sliced": "1.0.1" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + { + "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", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" } }, - "mongoose-legacy-pluralize": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", - "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" - }, - "mongose": { - "version": "0.0.2-security", - "resolved": "https://registry.npmjs.org/mongose/-/mongose-0.0.2-security.tgz", - "integrity": "sha512-XJUBQHhC/12+hWtrcB1Ww+gkxSzbxg4VdjpNlBQGvFoyPm1bErhA+3n/IkWbGCkavFB1OSycpvpCRphPsZXgLw==" - }, - "mpath": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.6.0.tgz", - "integrity": "sha512-i75qh79MJ5Xo/sbhxrDrPSEG0H/mr1kcZXJ8dH6URU5jD/knFxCVqVC/gVSW7GIXL/9hHWlT9haLbCXWOll3qw==" - }, - "mquery": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.2.tgz", - "integrity": "sha512-XB52992COp0KP230I3qloVUbkLUxJIu328HBP2t2EsxSFtf4W1HPSOBWOXf1bqxK4Xbb66lfMJ+Bpfd9/yZE1Q==", - "requires": { - "bluebird": "3.5.1", - "debug": "3.1.0", - "regexp-clone": "^1.0.0", - "safe-buffer": "5.1.2", - "sliced": "1.0.1" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" } }, - "ms": { + "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "multer": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.3.0.tgz", - "integrity": "sha1-CSsmcPaEb6SRSWXvyM+Uwg/sbNI=", - "requires": { - "append-field": "0.1.0", - "busboy": "0.2.14", - "concat-stream": "1.6.0", - "mkdirp": "0.5.1", - "object-assign": "3.0.0", - "on-finished": "2.3.0", - "type-is": "1.6.15", - "xtend": "4.0.1" - } - }, - "native-promise-only": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", - "integrity": "sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=" - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "parse-seconds": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-seconds/-/parse-seconds-1.0.0.tgz", - "integrity": "sha1-BoSBL/jexg1h47xww114w4D0acM=" + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "path-loader": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/path-loader/-/path-loader-1.0.4.tgz", - "integrity": "sha512-k/IPo9OWyofATP5gwIehHHQoFShS37zsSIsejKe6fjI+tqK+FnRpiSg4ZfWUpxb0g2PfCreWPqBD4ayjqjqkdQ==", - "requires": { - "native-promise-only": "0.8.1", - "superagent": "3.8.2" + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "proxy-addr": { + "node_modules/statuses": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", - "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", - "requires": { - "forwarded": "0.1.2", - "ipaddr.js": "1.5.2" + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, - "punycode": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", - "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=" - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", - "unpipe": "1.0.0" - } - }, - "readable-stream": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", - "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=", - "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" - } - }, - "regexp-clone": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz", - "integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==" + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } }, - "require_optional": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", - "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", - "requires": { - "resolve-from": "^2.0.0", - "semver": "^5.1.0" + "node_modules/stripe": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.3.1.tgz", + "integrity": "sha512-k990yOT5G5rhX3XluRPw5Y8RLdJDW4dzQ29wWT66piHrbnM2KyamJ1dKgPsw4HzGHRWjDiSSdcI2WdxQUPV3aQ==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@types/node": ">=16" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "resolve-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", - "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "send": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", - "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", - "requires": { - "debug": "2.6.9", - "depd": "1.1.1", - "destroy": "1.0.4", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "etag": "1.8.1", - "fresh": "0.5.2", - "http-errors": "1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "2.3.0", - "range-parser": "1.2.0", - "statuses": "1.3.1" - } - }, - "serve-static": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", - "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", - "requires": { - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "parseurl": "1.3.2", - "send": "0.16.1" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } }, - "sift": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/sift/-/sift-7.0.1.tgz", - "integrity": "sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g==" + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } }, - "sliced": { + "node_modules/toidentifier": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", - "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" - }, - "spark-md5": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spark-md5/-/spark-md5-3.0.0.tgz", - "integrity": "sha1-NyIifFTi+vJLHcbZM8wUTm9xv+8=" - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - }, - "streamsearch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" - }, - "string": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/string/-/string-3.3.3.tgz", - "integrity": "sha1-XqIRzZLSKOGEKUmQpsyXs2anfLA=" - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "superagent": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.2.tgz", - "integrity": "sha512-gVH4QfYHcY3P0f/BZzavLreHW3T1v7hG9B+hpMQotGQqurOvhv87GcMCd6LWySmBuf+BDR44TQd0aISjVHLeNQ==", - "requires": { - "component-emitter": "1.2.1", - "cookiejar": "2.1.1", - "debug": "3.1.0", - "extend": "3.0.1", - "form-data": "2.3.1", - "formidable": "1.1.1", - "methods": "1.1.2", - "mime": "1.4.1", - "qs": "6.5.1", - "readable-stream": "2.2.7" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" } }, - "swagger-converter": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/swagger-converter/-/swagger-converter-0.1.7.tgz", - "integrity": "sha1-oJdRnG8e5N1n4wjZtT3cnCslf5c=", - "requires": { - "lodash.clonedeep": "2.4.1" - } - }, - "swagger-tools": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/swagger-tools/-/swagger-tools-0.10.3.tgz", - "integrity": "sha512-2eepnAxniKB/oejo4pz4wGnN9hoXfLzs6ChVluDRCVzu98F7HDSRw0C+DwmiarXD5i1rjXK8yLvUuxQxOOKOJg==", - "requires": { - "async": "2.6.0", - "body-parser": "1.18.2", - "commander": "2.12.2", - "debug": "3.1.0", - "js-yaml": "3.10.0", - "json-refs": "3.0.2", - "lodash": "4.17.4", - "multer": "1.3.0", - "parseurl": "1.3.2", - "path-to-regexp": "2.1.0", - "qs": "6.5.1", - "serve-static": "1.13.1", - "spark-md5": "3.0.0", - "string": "3.3.3", - "superagent": "3.8.2", - "swagger-converter": "0.1.7", - "traverse": "0.6.6", - "z-schema": "3.19.0" - }, - "dependencies": { - "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", - "requires": { - "lodash": "4.17.4" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "path-to-regexp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.1.0.tgz", - "integrity": "sha512-dZY7QPCPp5r9cnNuQ955mOv4ZFVDXY/yvqeV7Y1W2PJA3PEFcuow9xKFfJxbBj1pIjOAP+M2B4/7xubmykLrXw==" - } + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" } }, - "traverse": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", - "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } }, - "type-is": { - "version": "1.6.15", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", - "requires": { + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { "media-typer": "0.3.0", - "mime-types": "2.1.17" + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" } }, - "typedarray": { + "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } }, - "underscore": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" }, - "underscore-plus": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/underscore-plus/-/underscore-plus-1.6.6.tgz", - "integrity": "sha1-ZezeG9xEGjXYnmUP1w3PE65Dmn0=", - "requires": { - "underscore": "1.6.0" + "node_modules/underscore-plus": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore-plus/-/underscore-plus-1.7.0.tgz", + "integrity": "sha512-A3BEzkeicFLnr+U/Q3EyWwJAQPbA19mtZZ4h+lLq3ttm9kn8WC4R3YpuJZEXmWdLjYP47Zc8aLZm9kwdv+zzvA==", + "dependencies": { + "underscore": "^1.9.1" } }, - "unpipe": { + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "uri-js": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", - "integrity": "sha1-+QuFhQf4HepNz7s8TD2/orVX+qo=", - "requires": { - "punycode": "2.1.0" + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, - "util-deprecate": { + "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" }, - "utils-merge": { + "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } }, - "validator": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-9.2.0.tgz", - "integrity": "sha512-6Ij4Eo0KM4LkR0d0IegOwluG5453uqT5QyF5SV5Ezvm8/zmkKI/L4eoraafZGlZPC9guLkwKzgypcw8VGWWnGA==" + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } }, - "vary": { + "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "z-schema": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.19.0.tgz", - "integrity": "sha512-V94f3ODuluBS4kQLLjNhwoMek0dyIXCsvNu/A17dAyJ6sMhT5KkJQwSn07R0naByLIXJWMDk+ruMfI/3G3hS4Q==", - "requires": { - "commander": "2.12.2", - "lodash.get": "4.4.2", - "lodash.isequal": "4.5.0", - "validator": "9.2.0" + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" } } } diff --git a/package.json b/package.json index 16b7515..effa8fb 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,37 @@ { - "name": "elm_chess_api", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "start": "node index.js" + "name": "chess-api", + "version": "2.0.0-alpha.1", + "private": true, + "type": "module", + "description": "General chess API (TypeScript)", + "scripts": { + "dev": "tsx watch src/main.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/main.js", + "test": "tsx --test test/**/*.test.ts" }, - "author": "Anze Mur", - "license": "ISC", "dependencies": { + "@fastify/cors": "^10.0.1", + "@fastify/rate-limit": "^10.1.0", + "@nestjs/common": "^10.4.2", + "@nestjs/config": "^3.2.3", + "@nestjs/core": "^10.4.2", + "@nestjs/platform-express": "^10.4.2", "chess-ai-kong": "^0.3.3", - "chess.js": "^0.10.2", - "express": "^4.16.2", - "hhmmss": "^1.0.0", - "mongoose": "^5.7.5", - "mongose": "0.0.2-security", - "parse-seconds": "^1.0.0", - "swagger-tools": "^0.10.3" + "chess.js": "^1.4.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "drizzle-orm": "^0.39.0", + "google-auth-library": "^9.15.1", + "postgres": "^3.4.5", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "stripe": "^20.3.1", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "tsx": "^4.19.2", + "typescript": "^5.7.3" } } diff --git a/postman/Chess API.postman_collection.json b/postman/Chess API.postman_collection.json deleted file mode 100644 index ec52770..0000000 --- a/postman/Chess API.postman_collection.json +++ /dev/null @@ -1,1179 +0,0 @@ -{ - "variables": [], - "info": { - "name": "Chess API", - "_postman_id": "ad2f0eee-233f-fc5c-123d-d3230fa72f28", - "description": "Chess API + highscores API", - "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" - }, - "item": [ - { - "name": "Highscores", - "description": "", - "item": [ - { - "name": "Create new local scoreboard", - "request": { - "url": { - "raw": "http://chess-api-chess.herokuapp.com/api/v1/scoreboard", - "protocol": "http", - "host": [ - "chess-api-chess", - "herokuapp", - "com" - ], - "path": [ - "api", - "v1", - "scoreboard" - ], - "query": [ - { - "key": "", - "value": "", - "equals": true, - "description": "", - "disabled": true - } - ], - "variable": [] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "scoreboard_id", - "value": "5a42ccb81b3f5e25cc4f0fc3", - "description": "", - "type": "text", - "disabled": true - }, - { - "key": "", - "value": "", - "description": "", - "type": "text", - "disabled": true - } - ] - }, - "description": "Create new local scoreboard : generates a scoreboard_id." - }, - "response": [] - }, - { - "name": "listTopFive", - "request": { - "url": { - "raw": "http://chess-api-chess.herokuapp.com/api/v1/highscores", - "protocol": "http", - "host": [ - "chess-api-chess", - "herokuapp", - "com" - ], - "path": [ - "api", - "v1", - "highscores" - ], - "query": [ - { - "key": "", - "value": "", - "equals": true, - "description": "", - "disabled": true - } - ], - "variable": [] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "scoreboard_id", - "value": "5a42ccb81b3f5e25cc4f0fc3", - "description": "", - "type": "text", - "disabled": true - }, - { - "key": "", - "value": "", - "description": "", - "type": "text", - "disabled": true - } - ] - }, - "description": "Lists top five players. If parameter scoreboard_id is added you get the local highscores." - }, - "response": [] - }, - { - "name": "addPlayer", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/highscores/add", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "name", - "value": "playerX", - "description": "", - "type": "text" - }, - { - "key": "score", - "value": "10", - "description": "", - "type": "text" - } - ] - }, - "description": "Adds a player to the scoreboard.\n" - }, - "response": [] - } - ] - }, - { - "name": "Chess", - "description": "", - "item": [ - { - "name": "One player game", - "description": "", - "item": [ - { - "name": "Gameplay", - "description": "", - "item": [ - { - "name": "Create new game", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one", - "method": "GET", - "header": [], - "body": {}, - "description": "Creates a new game and generates new game_id.\n" - }, - "response": [] - }, - { - "name": "List possible moves", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one/moves", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "position", - "value": "a2", - "description": "", - "type": "text" - }, - { - "key": "game_id", - "value": "5a43ca62772e4e00148e207b", - "description": "", - "type": "text" - } - ] - }, - "description": "Returns a list of legal moves from the current position." - }, - "response": [] - }, - { - "name": "Move figure player", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one/move/player", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "from", - "value": "a2", - "description": "", - "type": "text" - }, - { - "key": "to", - "value": "a3", - "description": "", - "type": "text" - }, - { - "key": "game_id", - "value": "5a43ca62772e4e00148e207b", - "description": "", - "type": "text" - } - ] - }, - "description": "Attempts to make a move on the board." - }, - "response": [] - }, - { - "name": "Move figure AI", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one/move/ai", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a43c4f3d326e114807278d2", - "description": "", - "type": "text" - } - ] - }, - "description": "AI attempts to make a move on the board." - }, - "response": [] - }, - { - "name": "Check game over", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one/check", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a3c356be4538a2628f17ca4", - "description": "", - "type": "text" - }, - { - "key": "", - "value": "", - "description": "", - "type": "text", - "disabled": true - } - ] - }, - "description": "Returns the current state of the game (checkmate, stalemate, draw, threefold repetition, or insufficient material, game continues)." - }, - "response": [] - }, - { - "name": "Check position statue", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one/check-position", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a3c356be4538a2628f17ca4", - "description": "", - "type": "text" - }, - { - "key": "position", - "value": "a2", - "description": "", - "type": "text", - "disabled": true - } - ] - }, - "description": "Returns the current state of a position on the board." - }, - "response": [] - } - ], - "_postman_isSubFolder": true - }, - { - "name": "Other options", - "description": "", - "item": [ - { - "name": "getFEN", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one/fen", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a3cf85420055432a4a430da", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "getAscii", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one/fen", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a3cf85420055432a4a430da", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "getPgn", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one/pgn", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a3cf85420055432a4a430da", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "getTurn", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one/turn", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a3cf98c91820501608bc67e", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "undoMove", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one/undo", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a43c4f3d326e114807278d2", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "resetBoard", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one/reset", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a43c4f3d326e114807278d2", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "clearBoard", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one/clear", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a43c4f3d326e114807278d2", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "Start new game from FEN", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one/start/fen", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a43c4f3d326e114807278d2", - "description": "", - "type": "text" - }, - { - "key": "fen", - "value": "4r3/8/2p2PPk/1p6/pP2p1R1/P1B5/2P2K2/3r4 w - - 1 45", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "Start new game from pgn", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one/start/fen", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a43c4f3d326e114807278d2", - "description": "", - "type": "text" - }, - { - "key": "pgn", - "value": "", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "Load FEN over current game", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one/load/over/fen", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a43c4f3d326e114807278d2", - "description": "", - "type": "text" - }, - { - "key": "fen", - "value": "4r3/8/2p2PPk/1p6/pP2p1R1/P1B5/2P2K2/3r4 w - - 1 45", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "Load pgn over current game", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/one/load/over/fen", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a43c4f3d326e114807278d2", - "description": "", - "type": "text" - }, - { - "key": "pgn", - "value": "", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - } - ], - "_postman_isSubFolder": true - } - ], - "_postman_isSubFolder": true - }, - { - "name": "Two players game", - "description": "", - "item": [ - { - "name": "Gameplay", - "description": "", - "item": [ - { - "name": "Create new game", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/two", - "method": "GET", - "header": [], - "body": {}, - "description": "Creates a new game and generates new game_id.\n" - }, - "response": [] - }, - { - "name": "List possible moves", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/two/moves", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "position", - "value": "a2", - "description": "", - "type": "text" - } - ] - }, - "description": "Returns a list of legal moves from the current position." - }, - "response": [] - }, - { - "name": "Move figure player", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/two/move", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "from", - "value": "a2", - "description": "", - "type": "text" - }, - { - "key": "to", - "value": "a3", - "description": "", - "type": "text" - }, - { - "key": "game_id", - "value": "5a43ca62772e4e00148e207b", - "description": "", - "type": "text" - } - ] - }, - "description": "Attempts to make a move on the board." - }, - "response": [] - }, - { - "name": "Check game over", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/two/check", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a3c356be4538a2628f17ca4", - "description": "", - "type": "text" - }, - { - "key": "", - "value": "", - "description": "", - "type": "text", - "disabled": true - } - ] - }, - "description": "Returns the current state of the game (checkmate, stalemate, draw, threefold repetition, or insufficient material, game continues)." - }, - "response": [] - }, - { - "name": "Check position statue", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/two/check-position", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a3c356be4538a2628f17ca4", - "description": "", - "type": "text" - }, - { - "key": "position", - "value": "a2", - "description": "", - "type": "text", - "disabled": true - } - ] - }, - "description": "Returns the current state of a position on the board." - }, - "response": [] - } - ], - "_postman_isSubFolder": true - }, - { - "name": "Other options", - "description": "", - "item": [ - { - "name": "getFEN", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/two/fen", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a3cf85420055432a4a430da", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "getAscii", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/two/fen", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a3cf85420055432a4a430da", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "getPgn", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/two/pgn", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a3cf85420055432a4a430da", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "getTurn", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/two/turn", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a3cf98c91820501608bc67e", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "undoMove", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/two/undo", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a43c4f3d326e114807278d2", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "resetBoard", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/two/reset", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a43c4f3d326e114807278d2", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "clearBoard", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/two/clear", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a43c4f3d326e114807278d2", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "Start new game from FEN", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/two/start/fen", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a43c4f3d326e114807278d2", - "description": "", - "type": "text" - }, - { - "key": "fen", - "value": "4r3/8/2p2PPk/1p6/pP2p1R1/P1B5/2P2K2/3r4 w - - 1 45", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "Start new game from pgn", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/two/start/fen", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a43c4f3d326e114807278d2", - "description": "", - "type": "text" - }, - { - "key": "pgn", - "value": "", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "Load FEN over current game", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/two/load/over/fen", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a43c4f3d326e114807278d2", - "description": "", - "type": "text" - }, - { - "key": "fen", - "value": "4r3/8/2p2PPk/1p6/pP2p1R1/P1B5/2P2K2/3r4 w - - 1 45", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "Load pgn over current game", - "request": { - "url": "http://chess-api-chess.herokuapp.com/api/v1/chess/two/load/over/fen", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "game_id", - "value": "5a43c4f3d326e114807278d2", - "description": "", - "type": "text" - }, - { - "key": "pgn", - "value": "", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - } - ], - "_postman_isSubFolder": true - } - ], - "_postman_isSubFolder": true - } - ] - } - ] -} \ No newline at end of file diff --git a/scripts/qa-checklist.sh b/scripts/qa-checklist.sh new file mode 100755 index 0000000..2e87216 --- /dev/null +++ b/scripts/qa-checklist.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +set -euo pipefail + +API_BASE_URL="${API_BASE_URL:-}" +GOOGLE_ID_TOKEN="${GOOGLE_ID_TOKEN:-}" +ACCESS_TOKEN="${ACCESS_TOKEN:-}" + +if [[ -z "$API_BASE_URL" ]]; then + echo "❌ Missing API_BASE_URL" + exit 1 +fi + +echo "== QA Checklist against $API_BASE_URL ==" + +check() { + local name="$1" + shift + if "$@" >/dev/null 2>&1; then + echo "✅ $name" + else + echo "❌ $name" + fi +} + +check "Health endpoint" curl -fsS "$API_BASE_URL/" + +if [[ -z "$ACCESS_TOKEN" && -n "$GOOGLE_ID_TOKEN" ]]; then + LOGIN_JSON=$(curl -fsS -X POST "$API_BASE_URL/auth/google" -H 'content-type: application/json' -d "{\"idToken\":\"$GOOGLE_ID_TOKEN\"}") + ACCESS_TOKEN=$(node -e "const x=JSON.parse(process.argv[1]); console.log(x.accessToken || '')" "$LOGIN_JSON") +fi + +if [[ -z "$ACCESS_TOKEN" ]]; then + echo "⚠️ Skipping auth-dependent checks (provide ACCESS_TOKEN or GOOGLE_ID_TOKEN)." + exit 0 +fi + +check "Google login/session works" curl -fsS "$API_BASE_URL/me" -H "Authorization: Bearer $ACCESS_TOKEN" + +KEY_JSON=$(curl -fsS -X POST "$API_BASE_URL/me/api-keys" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H 'content-type: application/json' \ + -d '{"name":"qa-checklist-primary"}') +API_KEY=$(node -e "const x=JSON.parse(process.argv[1]); console.log(x.apiKey || '')" "$KEY_JSON") + +if [[ -z "$API_KEY" ]]; then + echo "❌ API key lifecycle works" + exit 1 +fi + +echo "✅ API key lifecycle works" + +GAME_JSON=$(curl -fsS -X POST "$API_BASE_URL/games" \ + -H "x-api-key: $API_KEY" \ + -H 'content-type: application/json' \ + -d '{"mode":"pve","aiColor":"b","timeControl":{"initialSeconds":30,"incrementSeconds":1}}') +GAME_ID=$(node -e "const x=JSON.parse(process.argv[1]); console.log(x.id || '')" "$GAME_JSON") + +if [[ -z "$GAME_ID" ]]; then + echo "❌ Game flow works" + exit 1 +fi + +curl -fsS -X POST "$API_BASE_URL/games/$GAME_ID/moves" \ + -H "x-api-key: $API_KEY" \ + -H 'content-type: application/json' \ + -d '{"from":"e2","to":"e4"}' >/dev/null +curl -fsS -X POST "$API_BASE_URL/games/$GAME_ID/ai-move" -H "x-api-key: $API_KEY" >/dev/null +curl -fsS -X POST "$API_BASE_URL/games/$GAME_ID/resign" \ + -H "x-api-key: $API_KEY" \ + -H 'content-type: application/json' \ + -d '{"color":"w"}' >/dev/null + +echo "✅ Game flow works (create->move->ai->resign)" + +# Clock timeout check with very short clock +CLOCK_GAME_JSON=$(curl -fsS -X POST "$API_BASE_URL/games" \ + -H "x-api-key: $API_KEY" \ + -H 'content-type: application/json' \ + -d '{"mode":"pvp","timeControl":{"initialSeconds":1,"incrementSeconds":0}}') +CLOCK_GAME_ID=$(node -e "const x=JSON.parse(process.argv[1]); console.log(x.id || '')" "$CLOCK_GAME_JSON") + +if [[ -n "$CLOCK_GAME_ID" ]]; then + sleep 2 + CLOCK_STATE=$(curl -fsS "$API_BASE_URL/games/$CLOCK_GAME_ID" -H "x-api-key: $API_KEY") + CLOCK_STATUS=$(node -e "const x=JSON.parse(process.argv[1]); console.log(x.status || '')" "$CLOCK_STATE") + CLOCK_REASON=$(node -e "const x=JSON.parse(process.argv[1]); console.log(x.result?.reason || '')" "$CLOCK_STATE") + if [[ "$CLOCK_STATUS" == "finished" && "$CLOCK_REASON" == "timeout" ]]; then + echo "✅ Clock timeout works" + else + echo "⚠️ Clock timeout check inconclusive (status=$CLOCK_STATUS reason=$CLOCK_REASON)" + fi +else + echo "⚠️ Clock timeout check skipped (could not create timed game)" +fi + +# Docs examples smoke checks (same routes as docs snippets) +check "Docs example: list games" curl -fsS "$API_BASE_URL/games?status=active&limit=5" -H "x-api-key: $API_KEY" + +# Free-tier guard: third active key should fail (free max=2) +SECOND_KEY_STATUS=$(curl -s -o /tmp/qa_second_key.json -w "%{http_code}" -X POST "$API_BASE_URL/me/api-keys" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H 'content-type: application/json' \ + -d '{"name":"qa-checklist-second"}') +THIRD_KEY_STATUS=$(curl -s -o /tmp/qa_third_key.json -w "%{http_code}" -X POST "$API_BASE_URL/me/api-keys" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H 'content-type: application/json' \ + -d '{"name":"qa-checklist-third"}') + +if [[ "$SECOND_KEY_STATUS" == "201" && "$THIRD_KEY_STATUS" =~ ^(400|429)$ ]]; then + echo "✅ Free-tier limits enforced (API key cap)" +else + echo "⚠️ Free-tier limits check inconclusive (second=$SECOND_KEY_STATUS third=$THIRD_KEY_STATUS)" +fi + +if [[ -n "${STRIPE_TEST_PRICE_ID:-}" ]]; then + CHECKOUT_STATUS=$(curl -s -o /tmp/qa_checkout.json -w "%{http_code}" -X POST "$API_BASE_URL/billing/checkout-session" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H 'content-type: application/json' \ + -d "{\"priceId\":\"$STRIPE_TEST_PRICE_ID\"}") + if [[ "$CHECKOUT_STATUS" == "201" || "$CHECKOUT_STATUS" == "200" ]]; then + echo "✅ Upgrade-to-pro path reachable (checkout session)" + else + echo "⚠️ Upgrade-to-pro check inconclusive (checkout status=$CHECKOUT_STATUS)" + fi +else + echo "ℹ️ Upgrade-to-pro check skipped (set STRIPE_TEST_PRICE_ID to enable)." +fi + +echo "ℹ️ Webhook transition validation still requires Stripe event source + webhook secret wiring in staging." diff --git a/scripts/staging-verify.sh b/scripts/staging-verify.sh new file mode 100755 index 0000000..b72e196 --- /dev/null +++ b/scripts/staging-verify.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +set -euo pipefail + +API_BASE_URL="${API_BASE_URL:-}" +GOOGLE_ID_TOKEN="${GOOGLE_ID_TOKEN:-}" +ACCESS_TOKEN="${ACCESS_TOKEN:-}" +STRIPE_TEST_PRICE_ID="${STRIPE_TEST_PRICE_ID:-}" + +if [[ -z "$API_BASE_URL" ]]; then + echo "❌ Missing API_BASE_URL" + echo "Usage: API_BASE_URL=https://stg-api.example.com GOOGLE_ID_TOKEN= bash scripts/staging-verify.sh" + echo " or: API_BASE_URL=https://stg-api.example.com ACCESS_TOKEN= bash scripts/staging-verify.sh" + exit 1 +fi + +if [[ -z "$GOOGLE_ID_TOKEN" && -z "$ACCESS_TOKEN" ]]; then + echo "❌ Missing auth input" + echo "Provide GOOGLE_ID_TOKEN (preferred) or ACCESS_TOKEN." + exit 1 +fi + +echo "== Staging verification: $API_BASE_URL ==" + +echo "1) Health check" +curl -fsS "$API_BASE_URL/" >/dev/null + +if [[ -n "$GOOGLE_ID_TOKEN" ]]; then + echo "2) Auth login" + LOGIN_JSON=$(curl -fsS -X POST "$API_BASE_URL/auth/google" \ + -H 'content-type: application/json' \ + -d "{\"idToken\":\"$GOOGLE_ID_TOKEN\"}") + ACCESS_TOKEN=$(node -e "const x=JSON.parse(process.argv[1]);console.log(x.accessToken||'')" "$LOGIN_JSON") + if [[ -z "$ACCESS_TOKEN" ]]; then + echo "❌ Login failed: no access token" + echo "$LOGIN_JSON" + exit 1 + fi +else + echo "2) Auth login skipped (ACCESS_TOKEN provided)" +fi + +echo "3) Profile + plan" +curl -fsS "$API_BASE_URL/me" -H "Authorization: Bearer $ACCESS_TOKEN" >/dev/null +curl -fsS "$API_BASE_URL/me/plan" -H "Authorization: Bearer $ACCESS_TOKEN" >/dev/null + +echo "4) API key lifecycle (create/list/revoke)" +KEY_JSON=$(curl -fsS -X POST "$API_BASE_URL/me/api-keys" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H 'content-type: application/json' \ + -d '{"name":"staging-verify"}') +API_KEY=$(node -e "const x=JSON.parse(process.argv[1]);console.log(x.apiKey||'')" "$KEY_JSON") +if [[ -z "$API_KEY" ]]; then + echo "❌ API key creation failed" + echo "$KEY_JSON" + exit 1 +fi + +LIST_JSON=$(curl -fsS "$API_BASE_URL/me/api-keys" -H "Authorization: Bearer $ACCESS_TOKEN") +KEY_ID=$(node -e "const x=JSON.parse(process.argv[1]); const i=(x.items||[]).find((k)=>k.keyPrefix===process.argv[2].slice(0,12)); console.log(i?.id||'')" "$LIST_JSON" "$API_KEY") +if [[ -z "$KEY_ID" ]]; then + echo "❌ API key list missing created key" + echo "$LIST_JSON" + exit 1 +fi + +echo "5) Game flow" +GAME_JSON=$(curl -fsS -X POST "$API_BASE_URL/games" \ + -H "x-api-key: $API_KEY" \ + -H 'content-type: application/json' \ + -d '{"mode":"pve","aiColor":"b","timeControl":{"initialSeconds":120,"incrementSeconds":1}}') +GAME_ID=$(node -e "const x=JSON.parse(process.argv[1]);console.log(x.id||'')" "$GAME_JSON") +if [[ -z "$GAME_ID" ]]; then + echo "❌ Game creation failed" + echo "$GAME_JSON" + exit 1 +fi + +curl -fsS -X POST "$API_BASE_URL/games/$GAME_ID/moves" \ + -H "x-api-key: $API_KEY" \ + -H 'content-type: application/json' \ + -d '{"from":"e2","to":"e4"}' >/dev/null + +curl -fsS -X POST "$API_BASE_URL/games/$GAME_ID/ai-move" \ + -H "x-api-key: $API_KEY" >/dev/null + +curl -fsS -X POST "$API_BASE_URL/games/$GAME_ID/resign" \ + -H "x-api-key: $API_KEY" \ + -H 'content-type: application/json' \ + -d '{"color":"w"}' >/dev/null + +curl -fsS -X DELETE "$API_BASE_URL/me/api-keys/$KEY_ID" \ + -H "Authorization: Bearer $ACCESS_TOKEN" >/dev/null + +if [[ -n "$STRIPE_TEST_PRICE_ID" ]]; then + echo "6) Billing endpoints" + CHECKOUT_JSON=$(curl -fsS -X POST "$API_BASE_URL/billing/checkout-session" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H 'content-type: application/json' \ + -d "{\"priceId\":\"$STRIPE_TEST_PRICE_ID\"}") + CHECKOUT_URL=$(node -e "const x=JSON.parse(process.argv[1]);console.log(x.url||'')" "$CHECKOUT_JSON") + if [[ -z "$CHECKOUT_URL" ]]; then + echo "❌ Billing checkout session failed" + echo "$CHECKOUT_JSON" + exit 1 + fi + + PORTAL_JSON=$(curl -fsS -X POST "$API_BASE_URL/billing/portal-session" \ + -H "Authorization: Bearer $ACCESS_TOKEN") + PORTAL_URL=$(node -e "const x=JSON.parse(process.argv[1]);console.log(x.url||'')" "$PORTAL_JSON") + if [[ -z "$PORTAL_URL" ]]; then + echo "❌ Billing portal session failed" + echo "$PORTAL_JSON" + exit 1 + fi +fi + +echo "✅ Staging verification passed (auth + api key lifecycle + game flow${STRIPE_TEST_PRICE_ID:+ + billing endpoints})" diff --git a/scripts/staging-webhook-verify.sh b/scripts/staging-webhook-verify.sh new file mode 100755 index 0000000..58c8388 --- /dev/null +++ b/scripts/staging-webhook-verify.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +API_BASE_URL="${API_BASE_URL:-}" +STRIPE_WEBHOOK_SECRET="${STRIPE_WEBHOOK_SECRET:-}" +STRIPE_CUSTOMER_ID="${STRIPE_CUSTOMER_ID:-}" +USER_ID="${USER_ID:-}" + +if [[ -z "$API_BASE_URL" || -z "$STRIPE_WEBHOOK_SECRET" || -z "$STRIPE_CUSTOMER_ID" || -z "$USER_ID" ]]; then + echo "❌ Missing required envs." + echo "Required: API_BASE_URL, STRIPE_WEBHOOK_SECRET, STRIPE_CUSTOMER_ID, USER_ID" + echo "Optional: SUBSCRIPTION_ID (default=sub_test_123)" + exit 1 +fi + +SUBSCRIPTION_ID="${SUBSCRIPTION_ID:-sub_test_123}" + +payload_active=$(cat </dev/null +} + +echo "== Webhook entitlement verification against $API_BASE_URL ==" +echo "Sending active subscription event..." +send_event "$payload_active" + +echo "Sending canceled subscription event..." +send_event "$payload_canceled" + +echo "✅ Webhook events delivered. Now verify plan transition via:" +echo "curl -sS \"$API_BASE_URL/me/plan\" -H \"Authorization: Bearer \"" diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100644 index 0000000..23e9a0a --- /dev/null +++ b/src/app.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { DatabaseModule } from './database/database.module.js'; +import { AuthModule } from './modules/auth/auth.module.js'; +import { UsersModule } from './modules/users/users.module.js'; +import { ChessModule } from './modules/chess/chess.module.js'; +import { HealthModule } from './modules/health/health.module.js'; +import { BillingModule } from './modules/billing/billing.module.js'; + +@Module({ + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + DatabaseModule, + AuthModule, + UsersModule, + ChessModule, + HealthModule, + BillingModule, + ], +}) +export class AppModule {} diff --git a/src/common/filters/http-exception.filter.ts b/src/common/filters/http-exception.filter.ts new file mode 100644 index 0000000..59778c7 --- /dev/null +++ b/src/common/filters/http-exception.filter.ts @@ -0,0 +1,36 @@ +import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common'; + +@Catch() +export class HttpExceptionFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + const isHttp = exception instanceof HttpException; + const status = isHttp ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; + const raw = isHttp ? exception.getResponse() : 'Internal server error'; + + const message = + typeof raw === 'string' + ? raw + : Array.isArray((raw as any)?.message) + ? (raw as any).message.join(', ') + : ((raw as any)?.message ?? 'Internal server error'); + + const error = + typeof raw === 'object' && raw !== null && 'error' in (raw as Record) + ? String((raw as Record).error) + : HttpStatus[status] || 'Error'; + + response.status(status).json({ + error: { + statusCode: status, + code: error, + message, + }, + path: request?.url, + timestamp: new Date().toISOString(), + }); + } +} diff --git a/src/common/guards/api-key.guard.ts b/src/common/guards/api-key.guard.ts new file mode 100644 index 0000000..97659bf --- /dev/null +++ b/src/common/guards/api-key.guard.ts @@ -0,0 +1,84 @@ +import { CanActivate, ExecutionContext, HttpException, Injectable, UnauthorizedException } from '@nestjs/common'; +import { and, eq } from 'drizzle-orm'; +import { DatabaseService } from '../../database/database.service.js'; +import { apiKeys, apiUsageMonthly, plans } from '../../database/schema.js'; +import { randomId, sha256 } from '../../config/crypto.js'; + +const rpmBuckets = new Map(); + +export function resetApiKeyRateLimitBuckets() { + rpmBuckets.clear(); +} + +@Injectable() +export class ApiKeyGuard implements CanActivate { + constructor(private readonly dbs: DatabaseService) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + const key = req.headers['x-api-key'] as string | undefined; + if (!key) throw new UnauthorizedException('Missing x-api-key'); + + const keyHash = sha256(key); + const rows = await this.dbs.db + .select({ id: apiKeys.id, userId: apiKeys.userId }) + .from(apiKeys) + .where(and(eq(apiKeys.keyHash, keyHash), eq(apiKeys.active, true))) + .limit(1); + + const api = rows[0]; + if (!api) throw new UnauthorizedException('Invalid API key'); + + await this.enforcePlanLimits(api.userId, api.id); + + await this.dbs.db.update(apiKeys).set({ lastUsedAt: new Date() }).where(eq(apiKeys.id, api.id)); + req.apiUserId = api.userId; + return true; + } + + private async enforcePlanLimits(userId: string, apiKeyId: string) { + const [plan] = await this.dbs.db.select().from(plans).where(eq(plans.userId, userId)).limit(1); + const tier = plan?.tier === 'pro' ? 'pro' : 'free'; + + const rpmLimit = tier === 'pro' ? 300 : 30; + const monthlyLimit = tier === 'pro' ? 1_000_000 : 10_000; + + const now = Date.now(); + const bucketKey = `${apiKeyId}:${Math.floor(now / 60000)}`; + const bucket = rpmBuckets.get(bucketKey) ?? { start: now, count: 0 }; + bucket.count += 1; + rpmBuckets.set(bucketKey, bucket); + if (bucket.count > rpmLimit) { + throw new HttpException(`Rate limit exceeded for ${tier} plan (${rpmLimit} req/min)`, 429); + } + + const month = new Date().toISOString().slice(0, 7); + const existing = await this.dbs.db + .select({ id: apiUsageMonthly.id, requestCount: apiUsageMonthly.requestCount }) + .from(apiUsageMonthly) + .where(and(eq(apiUsageMonthly.userId, userId), eq(apiUsageMonthly.monthKey, month))) + .limit(1); + + if (!existing.length) { + await this.dbs.db.insert(apiUsageMonthly).values({ + id: randomId(), + userId, + monthKey: month, + requestCount: 1, + updatedAt: new Date(), + }); + return; + } + + const usage = existing[0]!; + const next = usage.requestCount + 1; + if (next > monthlyLimit) { + throw new HttpException(`Monthly quota exceeded for ${tier} plan (${monthlyLimit})`, 429); + } + + await this.dbs.db + .update(apiUsageMonthly) + .set({ requestCount: next, updatedAt: new Date() }) + .where(eq(apiUsageMonthly.id, usage.id)); + } +} diff --git a/src/common/guards/session.guard.ts b/src/common/guards/session.guard.ts new file mode 100644 index 0000000..b518091 --- /dev/null +++ b/src/common/guards/session.guard.ts @@ -0,0 +1,15 @@ +import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; +import { AuthService } from '../../modules/auth/auth.service.js'; + +@Injectable() +export class SessionGuard implements CanActivate { + constructor(private readonly auth: AuthService) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + const session = await this.auth.sessionFromBearer(req.headers.authorization); + if (!session) throw new UnauthorizedException('Invalid session'); + req.user = { id: session.userId, email: session.email, name: session.name }; + return true; + } +} diff --git a/src/common/middleware/request-logging.middleware.ts b/src/common/middleware/request-logging.middleware.ts new file mode 100644 index 0000000..2e9ab52 --- /dev/null +++ b/src/common/middleware/request-logging.middleware.ts @@ -0,0 +1,42 @@ +import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; +import { randomUUID } from 'node:crypto'; + +export function buildRequestLogLine(input: { + requestId: string; + method?: string; + url?: string; + statusCode?: number; + durationMs?: number; +}) { + const { requestId, method = 'UNKNOWN', url = '/', statusCode = 0, durationMs = 0 } = input; + return `[${requestId}] ${method} ${url} -> ${statusCode} ${durationMs}ms`; +} + +@Injectable() +export class RequestLoggingMiddleware implements NestMiddleware { + private readonly logger = new Logger('HTTP'); + + use(req: any, res: any, next: () => void) { + const startedAt = Date.now(); + const incoming = req.headers?.['x-request-id']; + const requestId = typeof incoming === 'string' && incoming.length > 5 ? incoming : randomUUID(); + + req.requestId = requestId; + res.setHeader('x-request-id', requestId); + + res.on('finish', () => { + const durationMs = Date.now() - startedAt; + this.logger.log( + buildRequestLogLine({ + requestId, + method: req.method, + url: req.originalUrl || req.url, + statusCode: res.statusCode, + durationMs, + }), + ); + }); + + next(); + } +} diff --git a/src/config/crypto.ts b/src/config/crypto.ts new file mode 100644 index 0000000..8996db1 --- /dev/null +++ b/src/config/crypto.ts @@ -0,0 +1,5 @@ +import crypto from 'node:crypto'; + +export const randomId = () => crypto.randomUUID(); +export const randomToken = (bytes = 32) => crypto.randomBytes(bytes).toString('hex'); +export const sha256 = (s: string) => crypto.createHash('sha256').update(s).digest('hex'); diff --git a/src/config/env.ts b/src/config/env.ts new file mode 100644 index 0000000..495fe17 --- /dev/null +++ b/src/config/env.ts @@ -0,0 +1,23 @@ +import { z } from 'zod' + +const EnvSchema = z.object({ + DATABASE_URL: z.string().url(), + GOOGLE_CLIENT_ID: z.string().min(10), + PORT: z.coerce.number().int().positive().default(3000), + APP_URL: z.string().url().default('http://localhost:3001'), + NEXT_PUBLIC_APP_URL: z.string().url().optional(), + STRIPE_SECRET_KEY: z.string().optional(), + STRIPE_WEBHOOK_SECRET: z.string().optional(), + STRIPE_PRICE_PRO: z.string().optional(), +}) + +export type Env = z.infer + +export function loadEnv(raw: NodeJS.ProcessEnv = process.env): Env { + const parsed = EnvSchema.safeParse(raw) + if (!parsed.success) { + const issues = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ') + throw new Error(`Invalid environment: ${issues}`) + } + return parsed.data +} diff --git a/src/database/database.module.ts b/src/database/database.module.ts new file mode 100644 index 0000000..fd2d5d8 --- /dev/null +++ b/src/database/database.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { DatabaseService } from './database.service.js'; + +@Global() +@Module({ + providers: [DatabaseService], + exports: [DatabaseService], +}) +export class DatabaseModule {} diff --git a/src/database/database.service.ts b/src/database/database.service.ts new file mode 100644 index 0000000..bb2ca1e --- /dev/null +++ b/src/database/database.service.ts @@ -0,0 +1,103 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; +import postgres from 'postgres'; +import { drizzle } from 'drizzle-orm/postgres-js'; + +@Injectable() +export class DatabaseService implements OnModuleInit { + readonly client; + readonly db; + + constructor() { + const url = process.env.DATABASE_URL; + if (!url) throw new Error('DATABASE_URL is required'); + this.client = postgres(url, { prepare: false }); + this.db = drizzle(this.client); + } + + async onModuleInit() { + await this.client` + create table if not exists users ( + id text primary key, + email text not null unique, + name text not null, + picture text, + created_at timestamptz not null default now() + );`; + await this.client` + create table if not exists sessions ( + token text primary key, + user_id text not null references users(id) on delete cascade, + expires_at timestamptz not null, + created_at timestamptz not null default now() + );`; + await this.client` + create table if not exists api_keys ( + id text primary key, + user_id text not null references users(id) on delete cascade, + name text not null, + key_hash text not null unique, + key_prefix text not null, + active boolean not null default true, + created_at timestamptz not null default now(), + last_used_at timestamptz + );`; + await this.client` + create table if not exists plans ( + user_id text primary key references users(id) on delete cascade, + tier text not null default 'free', + status text not null default 'active', + stripe_customer_id text, + stripe_subscription_id text, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() + );`; + await this.client` + create table if not exists api_usage_monthly ( + id text primary key, + user_id text not null references users(id) on delete cascade, + month_key text not null, + request_count integer not null default 0, + updated_at timestamptz not null default now() + );`; + await this.client` + create table if not exists players ( + id text primary key, + user_id text not null references users(id) on delete cascade, + display_name text not null, + rating integer not null default 1200, + external_app_user_id text, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() + );`; + await this.client` + create table if not exists games ( + id text primary key, + user_id text not null references users(id) on delete cascade, + mode text not null, + ai_color text, + fen text not null, + pgn text not null, + turn text not null, + status text not null, + result_reason text, + result_winner text, + history jsonb not null, + initial_ms integer, + increment_ms integer, + remaining_w integer, + remaining_b integer, + running text, + last_tick_at integer, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() + );`; + await this.client` + create table if not exists game_players ( + id text primary key, + game_id text not null references games(id) on delete cascade, + player_id text not null references players(id) on delete cascade, + color text not null, + created_at timestamptz not null default now() + );`; + } +} diff --git a/src/database/schema.ts b/src/database/schema.ts new file mode 100644 index 0000000..c680197 --- /dev/null +++ b/src/database/schema.ts @@ -0,0 +1,85 @@ +import { pgTable, text, timestamp, boolean, jsonb, integer } from 'drizzle-orm/pg-core'; + +export const users = pgTable('users', { + id: text('id').primaryKey(), + email: text('email').notNull().unique(), + name: text('name').notNull(), + picture: text('picture'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), +}); + +export const sessions = pgTable('sessions', { + token: text('token').primaryKey(), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), +}); + +export const apiKeys = pgTable('api_keys', { + id: text('id').primaryKey(), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + name: text('name').notNull(), + keyHash: text('key_hash').notNull().unique(), + keyPrefix: text('key_prefix').notNull(), + active: boolean('active').notNull().default(true), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + lastUsedAt: timestamp('last_used_at', { withTimezone: true }), +}); + +export const plans = pgTable('plans', { + userId: text('user_id').primaryKey().references(() => users.id, { onDelete: 'cascade' }), + tier: text('tier').notNull().default('free'), + status: text('status').notNull().default('active'), + stripeCustomerId: text('stripe_customer_id'), + stripeSubscriptionId: text('stripe_subscription_id'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}); + +export const apiUsageMonthly = pgTable('api_usage_monthly', { + id: text('id').primaryKey(), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + monthKey: text('month_key').notNull(), + requestCount: integer('request_count').notNull().default(0), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}); + +export const players = pgTable('players', { + id: text('id').primaryKey(), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + displayName: text('display_name').notNull(), + rating: integer('rating').notNull().default(1200), + externalAppUserId: text('external_app_user_id'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}); + +export const games = pgTable('games', { + id: text('id').primaryKey(), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + mode: text('mode').notNull(), + aiColor: text('ai_color'), + fen: text('fen').notNull(), + pgn: text('pgn').notNull(), + turn: text('turn').notNull(), + status: text('status').notNull(), + resultReason: text('result_reason'), + resultWinner: text('result_winner'), + history: jsonb('history').notNull().$type(), + initialMs: integer('initial_ms'), + incrementMs: integer('increment_ms'), + remainingW: integer('remaining_w'), + remainingB: integer('remaining_b'), + running: text('running'), + lastTickAt: integer('last_tick_at'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}); + +export const gamePlayers = pgTable('game_players', { + id: text('id').primaryKey(), + gameId: text('game_id').notNull().references(() => games.id, { onDelete: 'cascade' }), + playerId: text('player_id').notNull().references(() => players.id, { onDelete: 'cascade' }), + color: text('color').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), +}); diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..c3e61bc --- /dev/null +++ b/src/main.ts @@ -0,0 +1,18 @@ +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe } from '@nestjs/common'; +import { AppModule } from './app.module.js'; +import { loadEnv } from './config/env.js'; +import { HttpExceptionFilter } from './common/filters/http-exception.filter.js'; +import { RequestLoggingMiddleware } from './common/middleware/request-logging.middleware.js'; + +async function bootstrap() { + const env = loadEnv(); + const app = await NestFactory.create(AppModule); + app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); + const requestLogging = new RequestLoggingMiddleware(); + app.use(requestLogging.use.bind(requestLogging)); + app.useGlobalFilters(new HttpExceptionFilter()); + await app.listen(env.PORT); +} + +bootstrap(); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..ec7f809 --- /dev/null +++ b/src/modules/auth/auth.controller.ts @@ -0,0 +1,13 @@ +import { Body, Controller, Post } from '@nestjs/common'; +import { AuthService } from './auth.service.js'; +import { GoogleLoginDto } from './dto/google-login.dto.js'; + +@Controller('auth') +export class AuthController { + constructor(private readonly auth: AuthService) {} + + @Post('google') + google(@Body() body: GoogleLoginDto) { + return this.auth.loginWithGoogle(body.idToken); + } +} diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts new file mode 100644 index 0000000..5e39306 --- /dev/null +++ b/src/modules/auth/auth.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { AuthController } from './auth.controller.js'; +import { AuthService } from './auth.service.js'; + +@Module({ + controllers: [AuthController], + providers: [AuthService], + exports: [AuthService], +}) +export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..b61bba8 --- /dev/null +++ b/src/modules/auth/auth.service.ts @@ -0,0 +1,63 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { OAuth2Client } from 'google-auth-library'; +import { and, eq, gt } from 'drizzle-orm'; +import { DatabaseService } from '../../database/database.service.js'; +import { sessions, users } from '../../database/schema.js'; +import { randomId, randomToken } from '../../config/crypto.js'; + +@Injectable() +export class AuthService { + private google = new OAuth2Client(process.env.GOOGLE_CLIENT_ID); + + constructor(private readonly dbs: DatabaseService) {} + + async loginWithGoogle(idToken: string) { + if (!idToken) throw new BadRequestException('idToken is required'); + if (!process.env.GOOGLE_CLIENT_ID) throw new BadRequestException('GOOGLE_CLIENT_ID missing'); + + const ticket = await this.google.verifyIdToken({ idToken, audience: process.env.GOOGLE_CLIENT_ID }); + const payload = ticket.getPayload(); + if (!payload?.email || !payload.name) throw new BadRequestException('Invalid Google token payload'); + + const existing = await this.dbs.db.select().from(users).where(eq(users.email, payload.email)).limit(1); + const userId = existing[0]?.id ?? randomId(); + + if (!existing.length) { + await this.dbs.db.insert(users).values({ + id: userId, + email: payload.email, + name: payload.name, + picture: payload.picture ?? null, + }); + } + + const token = randomToken(32); + const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); + await this.dbs.db.insert(sessions).values({ token, userId, expiresAt }); + + return { + accessToken: token, + expiresAt, + user: { + id: userId, + email: payload.email, + name: payload.name, + picture: payload.picture ?? null, + }, + }; + } + + async sessionFromBearer(authHeader?: string) { + if (!authHeader?.startsWith('Bearer ')) return null; + const token = authHeader.slice('Bearer '.length); + + const rows = await this.dbs.db + .select({ userId: sessions.userId, email: users.email, name: users.name }) + .from(sessions) + .innerJoin(users, eq(users.id, sessions.userId)) + .where(and(eq(sessions.token, token), gt(sessions.expiresAt, new Date()))) + .limit(1); + + return rows[0] ?? null; + } +} diff --git a/src/modules/auth/dto/google-login.dto.ts b/src/modules/auth/dto/google-login.dto.ts new file mode 100644 index 0000000..fc469af --- /dev/null +++ b/src/modules/auth/dto/google-login.dto.ts @@ -0,0 +1,7 @@ +import { IsString, MinLength } from 'class-validator'; + +export class GoogleLoginDto { + @IsString() + @MinLength(10) + idToken!: string; +} diff --git a/src/modules/billing/billing.controller.ts b/src/modules/billing/billing.controller.ts new file mode 100644 index 0000000..f7165f8 --- /dev/null +++ b/src/modules/billing/billing.controller.ts @@ -0,0 +1,35 @@ +import { Body, Controller, Headers, Post, Req, UseGuards } from '@nestjs/common'; +import { SessionGuard } from '../../common/guards/session.guard.js'; +import { BillingService } from './billing.service.js'; +import { CreateCheckoutSessionDto } from './dto/create-checkout-session.dto.js'; + +@Controller('billing') +export class BillingController { + constructor(private readonly billing: BillingService) {} + + @UseGuards(SessionGuard) + @Post('checkout-session') + createCheckoutSession(@Req() req: any, @Body() body: CreateCheckoutSessionDto) { + return this.billing.createCheckoutSession({ + userId: req.user.id, + email: req.user.email, + name: req.user.name, + priceId: body.priceId, + }); + } + + @UseGuards(SessionGuard) + @Post('portal-session') + createPortalSession(@Req() req: any) { + return this.billing.createBillingPortalSession({ + userId: req.user.id, + email: req.user.email, + name: req.user.name, + }); + } + + @Post('webhook') + handleStripeWebhook(@Body() body: unknown, @Headers('stripe-signature') signature?: string) { + return this.billing.handleWebhook(body, signature); + } +} diff --git a/src/modules/billing/billing.module.ts b/src/modules/billing/billing.module.ts new file mode 100644 index 0000000..e89dd32 --- /dev/null +++ b/src/modules/billing/billing.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { BillingService } from './billing.service.js'; +import { BillingController } from './billing.controller.js'; +import { AuthModule } from '../auth/auth.module.js'; + +@Module({ + imports: [AuthModule], + controllers: [BillingController], + providers: [BillingService], + exports: [BillingService], +}) +export class BillingModule {} diff --git a/src/modules/billing/billing.service.ts b/src/modules/billing/billing.service.ts new file mode 100644 index 0000000..4a5d624 --- /dev/null +++ b/src/modules/billing/billing.service.ts @@ -0,0 +1,149 @@ +import { BadRequestException, Injectable, ServiceUnavailableException } from '@nestjs/common'; +import { eq, or } from 'drizzle-orm'; +import Stripe from 'stripe'; +import { DatabaseService } from '../../database/database.service.js'; +import { plans } from '../../database/schema.js'; + +@Injectable() +export class BillingService { + private stripe: Stripe | null = null; + + constructor(private readonly dbs: DatabaseService) {} + + private getStripeClient(): Stripe { + if (this.stripe) return this.stripe; + + const secretKey = process.env.STRIPE_SECRET_KEY; + if (!secretKey) { + throw new ServiceUnavailableException('Missing STRIPE_SECRET_KEY for billing actions'); + } + + this.stripe = new Stripe(secretKey); + return this.stripe; + } + + async ensureStripeCustomer(userId: string, email: string, name?: string) { + const [currentPlan] = await this.dbs.db + .select({ stripeCustomerId: plans.stripeCustomerId, tier: plans.tier, status: plans.status }) + .from(plans) + .where(eq(plans.userId, userId)) + .limit(1); + + if (currentPlan?.stripeCustomerId) { + return currentPlan.stripeCustomerId; + } + + const customer = await this.getStripeClient().customers.create({ + email, + name, + metadata: { userId }, + }); + + if (currentPlan) { + await this.dbs.db + .update(plans) + .set({ stripeCustomerId: customer.id, updatedAt: new Date() }) + .where(eq(plans.userId, userId)); + } else { + await this.dbs.db.insert(plans).values({ + userId, + tier: 'free', + status: 'active', + stripeCustomerId: customer.id, + updatedAt: new Date(), + }); + } + + return customer.id; + } + + async createCheckoutSession(input: { + userId: string; + email: string; + name?: string; + priceId?: string; + }) { + const stripe = this.getStripeClient(); + + const configuredPrice = process.env.STRIPE_PRICE_PRO; + const priceId = input.priceId || configuredPrice; + if (!priceId) { + throw new BadRequestException('Missing priceId and STRIPE_PRICE_PRO env'); + } + + const appUrl = process.env.APP_URL || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; + const customerId = await this.ensureStripeCustomer(input.userId, input.email, input.name); + + const session = await stripe.checkout.sessions.create({ + mode: 'subscription', + customer: customerId, + line_items: [{ price: priceId, quantity: 1 }], + success_url: `${appUrl}/dashboard?billing=success`, + cancel_url: `${appUrl}/dashboard?billing=cancelled`, + metadata: { userId: input.userId }, + allow_promotion_codes: true, + }); + + return { + sessionId: session.id, + url: session.url, + }; + } + + async createBillingPortalSession(input: { userId: string; email: string; name?: string }) { + const stripe = this.getStripeClient(); + const appUrl = process.env.APP_URL || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; + const customerId = await this.ensureStripeCustomer(input.userId, input.email, input.name); + + const session = await stripe.billingPortal.sessions.create({ + customer: customerId, + return_url: `${appUrl}/dashboard?billing=portal`, + }); + + return { + url: session.url, + }; + } + + async handleWebhook(body: unknown, stripeSignature?: string) { + const event = this.parseWebhookEvent(body, stripeSignature); + + if ( + event.type === 'customer.subscription.created' || + event.type === 'customer.subscription.updated' || + event.type === 'customer.subscription.deleted' + ) { + await this.applySubscriptionEvent(event.data.object as Stripe.Subscription); + } + + return { received: true }; + } + + private parseWebhookEvent(body: unknown, stripeSignature?: string): Stripe.Event { + const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; + + if (webhookSecret && stripeSignature) { + const payload = typeof body === 'string' ? body : JSON.stringify(body ?? {}); + return this.getStripeClient().webhooks.constructEvent(payload, stripeSignature, webhookSecret); + } + + return body as Stripe.Event; + } + + private async applySubscriptionEvent(subscription: Stripe.Subscription) { + const customerId = typeof subscription.customer === 'string' ? subscription.customer : subscription.customer?.id; + if (!customerId) return; + + const isActive = subscription.status === 'active' || subscription.status === 'trialing'; + await this.dbs.db + .update(plans) + .set({ + tier: isActive ? 'pro' : 'free', + status: subscription.status, + stripeCustomerId: customerId, + stripeSubscriptionId: subscription.id, + updatedAt: new Date(), + }) + .where(or(eq(plans.stripeCustomerId, customerId), eq(plans.userId, subscription.metadata?.userId || ''))); + } +} diff --git a/src/modules/billing/dto/create-checkout-session.dto.ts b/src/modules/billing/dto/create-checkout-session.dto.ts new file mode 100644 index 0000000..33f6dc6 --- /dev/null +++ b/src/modules/billing/dto/create-checkout-session.dto.ts @@ -0,0 +1,8 @@ +import { IsOptional, IsString, MinLength } from 'class-validator'; + +export class CreateCheckoutSessionDto { + @IsOptional() + @IsString() + @MinLength(3) + priceId?: string; +} diff --git a/src/modules/chess/chess.controller.ts b/src/modules/chess/chess.controller.ts new file mode 100644 index 0000000..c387e64 --- /dev/null +++ b/src/modules/chess/chess.controller.ts @@ -0,0 +1,84 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query, Req, UseGuards } from '@nestjs/common'; +import { ApiKeyGuard } from '../../common/guards/api-key.guard.js'; +import { ChessService } from './chess.service.js'; +import { CreateGameDto } from './dto/create-game.dto.js'; +import { MoveDto } from './dto/move.dto.js'; +import { ResignDto } from './dto/resign.dto.js'; +import { ListGamesDto } from './dto/list-games.dto.js'; +import { ListPlayersDto } from './dto/list-players.dto.js'; +import { CreatePlayerDto } from './dto/create-player.dto.js'; +import { AssignPlayerDto } from './dto/assign-player.dto.js'; +import { UpdatePlayerDto } from './dto/update-player.dto.js'; + +@Controller('games') +@UseGuards(ApiKeyGuard) +export class ChessController { + constructor(private readonly chess: ChessService) {} + + @Get() + list(@Req() req: any, @Query() query: ListGamesDto) { + return this.chess.list(req.apiUserId, query || {}); + } + + @Get('/players') + listPlayers(@Req() req: any, @Query() query: ListPlayersDto) { + return this.chess.listPlayers(req.apiUserId, query || {}); + } + + @Post('/players') + createPlayer(@Req() req: any, @Body() body: CreatePlayerDto) { + return this.chess.createPlayer(req.apiUserId, body); + } + + @Patch('/players/:playerId') + updatePlayer(@Req() req: any, @Param('playerId') playerId: string, @Body() body: UpdatePlayerDto) { + return this.chess.updatePlayer(req.apiUserId, playerId, body); + } + + @Delete('/players/:playerId') + async deletePlayer(@Req() req: any, @Param('playerId') playerId: string) { + await this.chess.deletePlayer(req.apiUserId, playerId); + return { ok: true }; + } + + @Post(':id/players') + assignPlayer(@Req() req: any, @Param('id') id: string, @Body() body: AssignPlayerDto) { + return this.chess.assignPlayerToGame(req.apiUserId, id, body); + } + + @Post() + create(@Req() req: any, @Body() body: CreateGameDto) { + return this.chess.create(req.apiUserId, body || {}); + } + + @Get(':id') + get(@Req() req: any, @Param('id') id: string) { + return this.chess.get(req.apiUserId, id); + } + + @Delete(':id') + async remove(@Req() req: any, @Param('id') id: string) { + await this.chess.remove(req.apiUserId, id); + return { ok: true }; + } + + @Get(':id/moves') + moves(@Req() req: any, @Param('id') id: string, @Query('from') from?: string) { + return this.chess.moves(req.apiUserId, id, from); + } + + @Post(':id/moves') + move(@Req() req: any, @Param('id') id: string, @Body() body: MoveDto) { + return this.chess.makeMove(req.apiUserId, id, body || {}); + } + + @Post(':id/ai-move') + aiMove(@Req() req: any, @Param('id') id: string) { + return this.chess.makeAiMove(req.apiUserId, id); + } + + @Post(':id/resign') + resign(@Req() req: any, @Param('id') id: string, @Body() body: ResignDto) { + return this.chess.makeResign(req.apiUserId, id, body.color); + } +} diff --git a/src/modules/chess/chess.engine.ts b/src/modules/chess/chess.engine.ts new file mode 100644 index 0000000..98de8df --- /dev/null +++ b/src/modules/chess/chess.engine.ts @@ -0,0 +1,189 @@ +import { Chess, type Square } from 'chess.js'; +import { createRequire } from 'node:module'; + +export type GameMode = 'pvp' | 'pve'; +export type Color = 'w' | 'b'; + +const require = createRequire(import.meta.url); +const chessAI: { play: (history: string[]) => string; setOptions: (opts: any) => void } = require('chess-ai-kong'); +chessAI.setOptions({ depth: 3, monitor: false, strategy: 'basic', timeout: 5000 }); + +export type GameState = { + mode: GameMode; + aiColor: Color | null; + chess: Chess; + status: 'active' | 'finished'; + result: { reason: 'checkmate' | 'draw' | 'timeout' | 'resign'; winner: Color | null } | null; + timeControl: null | { + initialMs: number; + incrementMs: number; + remaining: Record; + running: Color; + lastTickAt: number; + }; +}; + +const opposite = (c: Color): Color => (c === 'w' ? 'b' : 'w'); + +export function createState(input: { mode?: GameMode; fen?: string; aiColor?: Color; timeControl?: { initialSeconds: number; incrementSeconds?: number } }) { + const chess = new Chess(); + if (input.fen) chess.load(input.fen); + const turn = chess.turn(); + const tc = input.timeControl; + + const state: GameState = { + mode: input.mode ?? 'pvp', + aiColor: (input.mode ?? 'pvp') === 'pve' ? input.aiColor ?? 'b' : null, + chess, + status: 'active', + result: null, + timeControl: tc + ? { + initialMs: tc.initialSeconds * 1000, + incrementMs: (tc.incrementSeconds ?? 0) * 1000, + remaining: { w: tc.initialSeconds * 1000, b: tc.initialSeconds * 1000 }, + running: turn, + lastTickAt: Date.now(), + } + : null, + }; + return state; +} + +export function hydrate(row: any): GameState { + const chess = new Chess(row.fen); + return { + mode: row.mode, + aiColor: row.aiColor, + chess, + status: row.status, + result: row.resultReason ? { reason: row.resultReason, winner: row.resultWinner } : null, + timeControl: + row.initialMs !== null + ? { + initialMs: row.initialMs, + incrementMs: row.incrementMs, + remaining: { w: row.remainingW, b: row.remainingB }, + running: row.running, + lastTickAt: row.lastTickAt, + } + : null, + }; +} + +export function serialize(s: GameState) { + return { + mode: s.mode, + aiColor: s.aiColor, + fen: s.chess.fen(), + pgn: s.chess.pgn(), + turn: s.chess.turn(), + status: s.status, + resultReason: s.result?.reason ?? null, + resultWinner: s.result?.winner ?? null, + history: s.chess.history({ verbose: true }), + initialMs: s.timeControl?.initialMs ?? null, + incrementMs: s.timeControl?.incrementMs ?? null, + remainingW: s.timeControl?.remaining.w ?? null, + remainingB: s.timeControl?.remaining.b ?? null, + running: s.timeControl?.running ?? null, + lastTickAt: s.timeControl?.lastTickAt ?? null, + }; +} + +export function tick(s: GameState) { + if (!s.timeControl || s.status !== 'active') return; + const now = Date.now(); + const elapsed = Math.max(0, now - s.timeControl.lastTickAt); + s.timeControl.remaining[s.timeControl.running] = Math.max(0, s.timeControl.remaining[s.timeControl.running] - elapsed); + s.timeControl.lastTickAt = now; + if (s.timeControl.remaining[s.timeControl.running] <= 0) { + s.status = 'finished'; + s.result = { reason: 'timeout', winner: opposite(s.timeControl.running) }; + } +} + +function finalize(s: GameState, mover: Color) { + if (s.timeControl) { + s.timeControl.remaining[mover] += s.timeControl.incrementMs; + s.timeControl.running = s.chess.turn(); + s.timeControl.lastTickAt = Date.now(); + } + if (s.chess.isGameOver()) { + s.status = 'finished'; + s.result = s.chess.isCheckmate() ? { reason: 'checkmate', winner: opposite(s.chess.turn()) } : { reason: 'draw', winner: null }; + } +} + +export function legalMoves(s: GameState, from?: string) { + tick(s); + if (s.status !== 'active') return [] as string[]; + return from ? s.chess.moves({ square: from as Square }) : s.chess.moves(); +} + +export function move(s: GameState, payload: { san?: string; from?: string; to?: string; promotion?: 'q' | 'r' | 'b' | 'n' }) { + tick(s); + if (s.status !== 'active') return { error: 'Game is already finished' } as const; + const mover = s.chess.turn(); + let m: ReturnType | null = null; + + try { + m = payload.san + ? s.chess.move(payload.san) + : payload.from && payload.to + ? s.chess.move({ from: payload.from, to: payload.to, promotion: payload.promotion ?? 'q' }) + : null; + } catch { + m = null; + } + + if (!m) return { error: 'Illegal move' } as const; + finalize(s, mover); + return { move: m } as const; +} + +export async function aiMove(s: GameState) { + tick(s); + if (s.status !== 'active') return { error: 'Game is already finished' } as const; + if (s.mode !== 'pve') return { error: 'AI move only supported for pve games' } as const; + if (s.aiColor !== s.chess.turn()) return { error: 'Not AI turn' } as const; + + const mover = s.chess.turn(); + const legal = s.chess.moves(); + if (!legal.length) return { error: 'No legal AI moves' } as const; + + let m: ReturnType | null = null; + try { + const san = chessAI.play(s.chess.history()); + m = s.chess.move(san); + } catch { + m = null; + } + if (!m) m = s.chess.move(legal[Math.floor(Math.random() * legal.length)]!); + if (!m) return { error: 'AI failed to move' } as const; + finalize(s, mover); + return { move: m } as const; +} + +export function resign(s: GameState, color: Color) { + if (s.status !== 'active') return; + s.status = 'finished'; + s.result = { reason: 'resign', winner: opposite(color) }; +} + +export function present(id: string, s: GameState) { + tick(s); + return { + id, + mode: s.mode, + status: s.status, + result: s.result, + fen: s.chess.fen(), + pgn: s.chess.pgn(), + turn: s.chess.turn(), + history: s.chess.history({ verbose: true }), + timeControl: s.timeControl + ? { initialMs: s.timeControl.initialMs, incrementMs: s.timeControl.incrementMs, remaining: s.timeControl.remaining, running: s.timeControl.running } + : null, + }; +} diff --git a/src/modules/chess/chess.module.ts b/src/modules/chess/chess.module.ts new file mode 100644 index 0000000..0307461 --- /dev/null +++ b/src/modules/chess/chess.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ChessController } from './chess.controller.js'; +import { ChessService } from './chess.service.js'; + +@Module({ + controllers: [ChessController], + providers: [ChessService], +}) +export class ChessModule {} diff --git a/src/modules/chess/chess.service.ts b/src/modules/chess/chess.service.ts new file mode 100644 index 0000000..47f40eb --- /dev/null +++ b/src/modules/chess/chess.service.ts @@ -0,0 +1,255 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { and, desc, eq } from 'drizzle-orm'; +import { DatabaseService } from '../../database/database.service.js'; +import { gamePlayers, games, players } from '../../database/schema.js'; +import { randomId } from '../../config/crypto.js'; +import { aiMove, createState, hydrate, legalMoves, move, present, resign, serialize, type Color, type GameMode } from './chess.engine.js'; + +@Injectable() +export class ChessService { + constructor(private readonly dbs: DatabaseService) {} + + async list( + userId: string, + query: { mode?: GameMode; status?: 'active' | 'finished'; page?: number; limit?: number } + ) { + const page = Math.max(1, Number(query.page ?? 1)); + const limit = Math.min(100, Math.max(1, Number(query.limit ?? 20))); + + const conditions = [eq(games.userId, userId)]; + if (query.mode) conditions.push(eq(games.mode, query.mode)); + if (query.status) conditions.push(eq(games.status, query.status)); + + const rows = await this.dbs.db + .select({ + id: games.id, + mode: games.mode, + status: games.status, + turn: games.turn, + updatedAt: games.updatedAt, + createdAt: games.createdAt, + }) + .from(games) + .where(and(...conditions)) + .orderBy(desc(games.updatedAt)) + .limit(limit) + .offset((page - 1) * limit); + + return { page, limit, items: rows }; + } + + async listPlayers(userId: string, query: { page?: number; limit?: number }) { + const page = Math.max(1, Number(query.page ?? 1)); + const limit = Math.min(100, Math.max(1, Number(query.limit ?? 20))); + + const rows = await this.dbs.db + .select({ + id: players.id, + displayName: players.displayName, + rating: players.rating, + externalAppUserId: players.externalAppUserId, + createdAt: players.createdAt, + updatedAt: players.updatedAt, + }) + .from(players) + .where(eq(players.userId, userId)) + .orderBy(desc(players.updatedAt)) + .limit(limit) + .offset((page - 1) * limit); + + return { page, limit, items: rows }; + } + + async createPlayer( + userId: string, + body: { displayName: string; rating?: number; externalAppUserId?: string } + ) { + const id = randomId(); + await this.dbs.db.insert(players).values({ + id, + userId, + displayName: body.displayName.trim(), + rating: body.rating ?? 1200, + externalAppUserId: body.externalAppUserId ?? null, + updatedAt: new Date(), + }); + + const rows = await this.dbs.db + .select({ + id: players.id, + displayName: players.displayName, + rating: players.rating, + externalAppUserId: players.externalAppUserId, + createdAt: players.createdAt, + updatedAt: players.updatedAt, + }) + .from(players) + .where(eq(players.id, id)) + .limit(1); + + return rows[0]!; + } + + async updatePlayer( + userId: string, + playerId: string, + body: { displayName?: string; rating?: number; externalAppUserId?: string } + ) { + const existing = await this.dbs.db + .select({ id: players.id }) + .from(players) + .where(and(eq(players.id, playerId), eq(players.userId, userId))) + .limit(1); + + if (!existing.length) throw new NotFoundException('Player not found'); + + const patch: { + displayName?: string; + rating?: number; + externalAppUserId?: string; + updatedAt: Date; + } = { updatedAt: new Date() }; + + if (body.displayName !== undefined) patch.displayName = body.displayName.trim(); + if (body.rating !== undefined) patch.rating = body.rating; + if (body.externalAppUserId !== undefined) patch.externalAppUserId = body.externalAppUserId; + + await this.dbs.db + .update(players) + .set(patch) + .where(and(eq(players.id, playerId), eq(players.userId, userId))); + + const rows = await this.dbs.db + .select({ + id: players.id, + displayName: players.displayName, + rating: players.rating, + externalAppUserId: players.externalAppUserId, + createdAt: players.createdAt, + updatedAt: players.updatedAt, + }) + .from(players) + .where(and(eq(players.id, playerId), eq(players.userId, userId))) + .limit(1); + + return rows[0]!; + } + + async deletePlayer(userId: string, playerId: string) { + const existing = await this.dbs.db + .select({ id: players.id }) + .from(players) + .where(and(eq(players.id, playerId), eq(players.userId, userId))) + .limit(1); + + if (!existing.length) throw new NotFoundException('Player not found'); + + await this.dbs.db.delete(players).where(and(eq(players.id, playerId), eq(players.userId, userId))); + } + + async assignPlayerToGame( + userId: string, + gameId: string, + body: { playerId: string; color: 'w' | 'b' } + ) { + await this.load(userId, gameId); // ownership check + + const ownedPlayer = await this.dbs.db + .select({ id: players.id }) + .from(players) + .where(and(eq(players.id, body.playerId), eq(players.userId, userId))) + .limit(1); + + if (!ownedPlayer.length) { + throw new NotFoundException('Player not found'); + } + + const existing = await this.dbs.db + .select({ id: gamePlayers.id }) + .from(gamePlayers) + .where(and(eq(gamePlayers.gameId, gameId), eq(gamePlayers.color, body.color))) + .limit(1); + + if (existing.length) { + await this.dbs.db + .update(gamePlayers) + .set({ playerId: body.playerId }) + .where(eq(gamePlayers.id, existing[0]!.id)); + } else { + await this.dbs.db.insert(gamePlayers).values({ + id: randomId(), + gameId, + playerId: body.playerId, + color: body.color, + }); + } + + return { ok: true }; + } + + async create(userId: string, body: { mode?: GameMode; fen?: string; aiColor?: Color; timeControl?: { initialSeconds: number; incrementSeconds?: number } }) { + const id = randomId(); + const state = createState(body); + await this.dbs.db.insert(games).values({ id, userId, ...serialize(state), updatedAt: new Date() }); + return present(id, state); + } + + async get(userId: string, id: string) { + const row = await this.load(userId, id); + const state = hydrate(row); + await this.save(userId, id, state); + return present(id, state); + } + + async remove(userId: string, id: string) { + await this.dbs.db.delete(games).where(and(eq(games.id, id), eq(games.userId, userId))); + } + + async moves(userId: string, id: string, from?: string) { + const row = await this.load(userId, id); + const state = hydrate(row); + const m = legalMoves(state, from); + await this.save(userId, id, state); + return { gameId: id, from: from ?? null, count: m.length, moves: m }; + } + + async makeMove(userId: string, id: string, body: { san?: string; from?: string; to?: string; promotion?: 'q' | 'r' | 'b' | 'n' }) { + const row = await this.load(userId, id); + const state = hydrate(row); + const r = move(state, body); + if ('error' in r) return r; + await this.save(userId, id, state); + return { move: r.move, state: present(id, state) }; + } + + async makeAiMove(userId: string, id: string) { + const row = await this.load(userId, id); + const state = hydrate(row); + const r = await aiMove(state); + if ('error' in r) return r; + await this.save(userId, id, state); + return { move: r.move, state: present(id, state) }; + } + + async makeResign(userId: string, id: string, color: Color) { + const row = await this.load(userId, id); + const state = hydrate(row); + resign(state, color); + await this.save(userId, id, state); + return present(id, state); + } + + private async load(userId: string, id: string) { + const rows = await this.dbs.db.select().from(games).where(and(eq(games.id, id), eq(games.userId, userId))).limit(1); + const row = rows[0]; + if (!row) throw new NotFoundException('Game not found'); + return row as any; + } + + private async save(userId: string, id: string, state: any) { + await this.dbs.db + .update(games) + .set({ ...serialize(state), updatedAt: new Date() }) + .where(and(eq(games.id, id), eq(games.userId, userId))); + } +} diff --git a/src/modules/chess/dto/assign-player.dto.ts b/src/modules/chess/dto/assign-player.dto.ts new file mode 100644 index 0000000..ac59f67 --- /dev/null +++ b/src/modules/chess/dto/assign-player.dto.ts @@ -0,0 +1,10 @@ +import { IsIn, IsString, MinLength } from 'class-validator'; + +export class AssignPlayerDto { + @IsString() + @MinLength(3) + playerId!: string; + + @IsIn(['w', 'b']) + color!: 'w' | 'b'; +} diff --git a/src/modules/chess/dto/create-game.dto.ts b/src/modules/chess/dto/create-game.dto.ts new file mode 100644 index 0000000..9c0f360 --- /dev/null +++ b/src/modules/chess/dto/create-game.dto.ts @@ -0,0 +1,35 @@ +import { Type } from 'class-transformer'; +import { IsIn, IsInt, IsObject, IsOptional, IsString, Min, ValidateNested } from 'class-validator'; + +class TimeControlDto { + @Type(() => Number) + @IsInt() + @Min(1) + initialSeconds!: number; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + incrementSeconds?: number; +} + +export class CreateGameDto { + @IsOptional() + @IsIn(['pvp', 'pve']) + mode?: 'pvp' | 'pve'; + + @IsOptional() + @IsString() + fen?: string; + + @IsOptional() + @IsIn(['w', 'b']) + aiColor?: 'w' | 'b'; + + @IsOptional() + @IsObject() + @ValidateNested() + @Type(() => TimeControlDto) + timeControl?: TimeControlDto; +} diff --git a/src/modules/chess/dto/create-player.dto.ts b/src/modules/chess/dto/create-player.dto.ts new file mode 100644 index 0000000..868a01b --- /dev/null +++ b/src/modules/chess/dto/create-player.dto.ts @@ -0,0 +1,18 @@ +import { IsInt, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator'; + +export class CreatePlayerDto { + @IsString() + @MaxLength(80) + displayName!: string; + + @IsOptional() + @IsInt() + @Min(100) + @Max(3500) + rating?: number; + + @IsOptional() + @IsString() + @MaxLength(120) + externalAppUserId?: string; +} diff --git a/src/modules/chess/dto/list-games.dto.ts b/src/modules/chess/dto/list-games.dto.ts new file mode 100644 index 0000000..ccae78c --- /dev/null +++ b/src/modules/chess/dto/list-games.dto.ts @@ -0,0 +1,25 @@ +import { Type } from 'class-transformer'; +import { IsIn, IsInt, IsOptional, Max, Min } from 'class-validator'; + +export class ListGamesDto { + @IsOptional() + @IsIn(['pvp', 'pve']) + mode?: 'pvp' | 'pve'; + + @IsOptional() + @IsIn(['active', 'finished']) + status?: 'active' | 'finished'; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = 1; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit?: number = 20; +} diff --git a/src/modules/chess/dto/list-players.dto.ts b/src/modules/chess/dto/list-players.dto.ts new file mode 100644 index 0000000..d4bff51 --- /dev/null +++ b/src/modules/chess/dto/list-players.dto.ts @@ -0,0 +1,17 @@ +import { Type } from 'class-transformer'; +import { IsInt, IsOptional, Max, Min } from 'class-validator'; + +export class ListPlayersDto { + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit?: number; +} diff --git a/src/modules/chess/dto/move.dto.ts b/src/modules/chess/dto/move.dto.ts new file mode 100644 index 0000000..02fbc03 --- /dev/null +++ b/src/modules/chess/dto/move.dto.ts @@ -0,0 +1,21 @@ +import { IsIn, IsOptional, IsString, Length } from 'class-validator'; + +export class MoveDto { + @IsOptional() + @IsString() + san?: string; + + @IsOptional() + @IsString() + @Length(2, 2) + from?: string; + + @IsOptional() + @IsString() + @Length(2, 2) + to?: string; + + @IsOptional() + @IsIn(['q', 'r', 'b', 'n']) + promotion?: 'q' | 'r' | 'b' | 'n'; +} diff --git a/src/modules/chess/dto/resign.dto.ts b/src/modules/chess/dto/resign.dto.ts new file mode 100644 index 0000000..8493726 --- /dev/null +++ b/src/modules/chess/dto/resign.dto.ts @@ -0,0 +1,6 @@ +import { IsIn } from 'class-validator'; + +export class ResignDto { + @IsIn(['w', 'b']) + color!: 'w' | 'b'; +} diff --git a/src/modules/chess/dto/update-player.dto.ts b/src/modules/chess/dto/update-player.dto.ts new file mode 100644 index 0000000..c6bdbdd --- /dev/null +++ b/src/modules/chess/dto/update-player.dto.ts @@ -0,0 +1,19 @@ +import { IsInt, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator'; + +export class UpdatePlayerDto { + @IsOptional() + @IsString() + @MaxLength(80) + displayName?: string; + + @IsOptional() + @IsInt() + @Min(100) + @Max(3500) + rating?: number; + + @IsOptional() + @IsString() + @MaxLength(120) + externalAppUserId?: string; +} diff --git a/src/modules/health/health.controller.ts b/src/modules/health/health.controller.ts new file mode 100644 index 0000000..cd54653 --- /dev/null +++ b/src/modules/health/health.controller.ts @@ -0,0 +1,26 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller() +export class HealthController { + @Get() + root() { + return { ok: true, service: 'chess-api', docs: '/api' }; + } + + @Get('api') + api() { + return { + message: 'Chess API (NestJS modular rewrite)', + auth: ['POST /auth/google', 'GET /me', 'GET/POST/DELETE /me/api-keys'], + chess: [ + 'POST /games', + 'GET /games/:id', + 'DELETE /games/:id', + 'GET /games/:id/moves', + 'POST /games/:id/moves', + 'POST /games/:id/ai-move', + 'POST /games/:id/resign', + ], + }; + } +} diff --git a/src/modules/health/health.module.ts b/src/modules/health/health.module.ts new file mode 100644 index 0000000..4bc6527 --- /dev/null +++ b/src/modules/health/health.module.ts @@ -0,0 +1,5 @@ +import { Module } from '@nestjs/common'; +import { HealthController } from './health.controller.js'; + +@Module({ controllers: [HealthController] }) +export class HealthModule {} diff --git a/src/modules/users/dto/create-api-key.dto.ts b/src/modules/users/dto/create-api-key.dto.ts new file mode 100644 index 0000000..5344ecd --- /dev/null +++ b/src/modules/users/dto/create-api-key.dto.ts @@ -0,0 +1,8 @@ +import { IsOptional, IsString, MaxLength } from 'class-validator'; + +export class CreateApiKeyDto { + @IsOptional() + @IsString() + @MaxLength(80) + name?: string; +} diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts new file mode 100644 index 0000000..c8584d8 --- /dev/null +++ b/src/modules/users/users.controller.ts @@ -0,0 +1,36 @@ +import { Body, Controller, Delete, Get, Param, Post, Req, UseGuards } from '@nestjs/common'; +import { SessionGuard } from '../../common/guards/session.guard.js'; +import { UsersService } from './users.service.js'; +import { CreateApiKeyDto } from './dto/create-api-key.dto.js'; + +@Controller('me') +@UseGuards(SessionGuard) +export class UsersController { + constructor(private readonly users: UsersService) {} + + @Get() + me(@Req() req: any) { + return { user: req.user }; + } + + @Get('plan') + async plan(@Req() req: any) { + return { plan: await this.users.getPlan(req.user.id) }; + } + + @Get('api-keys') + async list(@Req() req: any) { + return { items: await this.users.listApiKeys(req.user.id) }; + } + + @Post('api-keys') + create(@Req() req: any, @Body() body: CreateApiKeyDto) { + return this.users.createApiKey(req.user.id, body?.name || 'default'); + } + + @Delete('api-keys/:id') + async revoke(@Req() req: any, @Param('id') id: string) { + await this.users.revokeApiKey(req.user.id, id); + return { ok: true }; + } +} diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts new file mode 100644 index 0000000..b4e07cb --- /dev/null +++ b/src/modules/users/users.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { UsersController } from './users.controller.js'; +import { UsersService } from './users.service.js'; +import { AuthModule } from '../auth/auth.module.js'; + +@Module({ + imports: [AuthModule], + controllers: [UsersController], + providers: [UsersService], +}) +export class UsersModule {} diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts new file mode 100644 index 0000000..cc24009 --- /dev/null +++ b/src/modules/users/users.service.ts @@ -0,0 +1,62 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { and, eq } from 'drizzle-orm'; +import { DatabaseService } from '../../database/database.service.js'; +import { apiKeys, plans } from '../../database/schema.js'; +import { randomId, randomToken, sha256 } from '../../config/crypto.js'; + +@Injectable() +export class UsersService { + constructor(private readonly dbs: DatabaseService) {} + + async getPlan(userId: string) { + const rows = await this.dbs.db.select().from(plans).where(eq(plans.userId, userId)).limit(1); + if (!rows.length) { + await this.dbs.db.insert(plans).values({ userId, tier: 'free', status: 'active', updatedAt: new Date() }); + return { tier: 'free', status: 'active' }; + } + return rows[0]; + } + + async listApiKeys(userId: string) { + return this.dbs.db + .select({ + id: apiKeys.id, + name: apiKeys.name, + keyPrefix: apiKeys.keyPrefix, + active: apiKeys.active, + createdAt: apiKeys.createdAt, + lastUsedAt: apiKeys.lastUsedAt, + }) + .from(apiKeys) + .where(eq(apiKeys.userId, userId)); + } + + async createApiKey(userId: string, name = 'default') { + const plan = await this.getPlan(userId); + const existing = await this.listApiKeys(userId); + const activeCount = existing.filter((k) => k.active).length; + + const maxKeys = plan.tier === 'pro' ? 20 : 2; + if (activeCount >= maxKeys) { + throw new BadRequestException(`API key limit reached for ${plan.tier} plan (max ${maxKeys})`); + } + + const rawKey = `chess_${randomToken(24)}`; + await this.dbs.db.insert(apiKeys).values({ + id: randomId(), + userId, + name: name.slice(0, 80), + keyHash: sha256(rawKey), + keyPrefix: rawKey.slice(0, 12), + active: true, + }); + return { apiKey: rawKey }; + } + + async revokeApiKey(userId: string, id: string) { + await this.dbs.db + .update(apiKeys) + .set({ active: false }) + .where(and(eq(apiKeys.id, id), eq(apiKeys.userId, userId))); + } +} diff --git a/test/api-key.guard.test.ts b/test/api-key.guard.test.ts new file mode 100644 index 0000000..9596dff --- /dev/null +++ b/test/api-key.guard.test.ts @@ -0,0 +1,76 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { ApiKeyGuard, resetApiKeyRateLimitBuckets } from '../src/common/guards/api-key.guard.js'; + +function makeContext(key: string) { + const req: any = { headers: { 'x-api-key': key } }; + return { + switchToHttp: () => ({ getRequest: () => req }), + } as any; +} + +function makeDbMock(opts?: { tier?: 'free' | 'pro'; monthly?: number; apiId?: string; userId?: string }) { + const state = { + tier: opts?.tier ?? 'free', + monthly: opts?.monthly ?? 0, + apiId: opts?.apiId ?? 'k1', + userId: opts?.userId ?? 'u1', + }; + + let selectCall = 0; + const db: any = { + select: () => ({ + from: () => ({ + where: () => ({ + limit: async () => { + selectCall = (selectCall % 3) + 1; + if (selectCall === 1) return [{ id: state.apiId, userId: state.userId }]; // api_keys + if (selectCall === 2) return [{ tier: state.tier }]; // plans + if (selectCall === 3) return state.monthly > 0 ? [{ id: 'm1', requestCount: state.monthly }] : []; // usage + return []; + }, + }), + }), + }), + insert: () => ({ values: async (v: any) => { state.monthly = v.requestCount; } }), + update: () => ({ set: (v: any) => ({ where: async () => { if (typeof v.requestCount === 'number') state.monthly = v.requestCount; } }) }), + }; + + return { db, state }; +} + +test('api key guard allows request and sets apiUserId', async () => { + resetApiKeyRateLimitBuckets(); + const mock = makeDbMock({ monthly: 0, apiId: 'k-allow' }); + const guard = new ApiKeyGuard(mock as any); + const context = makeContext('chess_key_123'); + const ok = await guard.canActivate(context); + assert.equal(ok, true); + assert.equal(context.switchToHttp().getRequest().apiUserId, 'u1'); +}); + +test('api key guard enforces free monthly quota', async () => { + resetApiKeyRateLimitBuckets(); + const mock = makeDbMock({ tier: 'free', monthly: 10000, apiId: 'k-monthly' }); + const guard = new ApiKeyGuard(mock as any); + const context = makeContext('chess_key_abc'); + await assert.rejects(() => guard.canActivate(context)); +}); + +test('api key RPM limiting is isolated per key (not global)', async () => { + resetApiKeyRateLimitBuckets(); + + const mockA = makeDbMock({ tier: 'free', monthly: 0, apiId: 'key-A' }); + const guardA = new ApiKeyGuard(mockA as any); + + for (let i = 0; i < 30; i++) { + const ok = await guardA.canActivate(makeContext('chess_key_A')); + assert.equal(ok, true); + } + await assert.rejects(() => guardA.canActivate(makeContext('chess_key_A'))); + + const mockB = makeDbMock({ tier: 'free', monthly: 0, apiId: 'key-B' }); + const guardB = new ApiKeyGuard(mockB as any); + const okB = await guardB.canActivate(makeContext('chess_key_B')); + assert.equal(okB, true); +}); diff --git a/test/auth.module.test.ts b/test/auth.module.test.ts new file mode 100644 index 0000000..5bb688e --- /dev/null +++ b/test/auth.module.test.ts @@ -0,0 +1,43 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { AuthService } from '../src/modules/auth/auth.service.js'; + +function makeDbMock() { + const db: any = {}; + db.select = () => ({ + from: () => ({ + where: () => ({ + limit: async () => [], + }), + innerJoin: () => ({ + where: () => ({ limit: async () => [] }), + }), + }), + }); + db.insert = () => ({ values: async () => undefined }); + return { db }; +} + +test('auth service returns null for missing bearer', async () => { + const auth = new AuthService(makeDbMock() as any); + const session = await auth.sessionFromBearer(undefined); + assert.equal(session, null); +}); + +test('auth service loginWithGoogle validates token', async () => { + const prev = process.env.GOOGLE_CLIENT_ID; + process.env.GOOGLE_CLIENT_ID = 'test-client-id'; + const auth = new AuthService(makeDbMock() as any); + + (auth as any).google = { + verifyIdToken: async () => ({ + getPayload: () => ({ email: 'a@b.com', name: 'A', picture: null }), + }), + }; + + const result = await auth.loginWithGoogle('valid-id-token-123'); + assert.equal(typeof result.accessToken, 'string'); + assert.equal(result.user.email, 'a@b.com'); + + process.env.GOOGLE_CLIENT_ID = prev; +}); diff --git a/test/billing.module.test.ts b/test/billing.module.test.ts new file mode 100644 index 0000000..a110661 --- /dev/null +++ b/test/billing.module.test.ts @@ -0,0 +1,218 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { BillingService } from '../src/modules/billing/billing.service.js'; + +function makeDbMock(planRow: any = null) { + const calls: any = { inserted: null, updated: null }; + const db: any = { + select: () => ({ from: () => ({ where: () => ({ limit: async () => (planRow ? [planRow] : []) }) }) }), + insert: () => ({ values: async (v: any) => (calls.inserted = v) }), + update: () => ({ set: (v: any) => ({ where: async () => (calls.updated = v) }) }), + }; + return { db, calls }; +} + +test('billing service returns existing stripe customer id', async () => { + const mock = makeDbMock({ stripeCustomerId: 'cus_existing', tier: 'pro', status: 'active' }); + const billing = new BillingService(mock as any); + + const out = await billing.ensureStripeCustomer('u1', 'u1@example.com'); + assert.equal(out, 'cus_existing'); + assert.equal(mock.calls.inserted, null); + assert.equal(mock.calls.updated, null); +}); + +test('billing service creates stripe customer and updates existing plan row', async () => { + const mock = makeDbMock({ stripeCustomerId: null, tier: 'free', status: 'active' }); + const billing = new BillingService(mock as any); + + (billing as any).stripe = { + customers: { + create: async () => ({ id: 'cus_new' }), + }, + }; + + const out = await billing.ensureStripeCustomer('u1', 'u1@example.com', 'User One'); + assert.equal(out, 'cus_new'); + assert.equal(mock.calls.updated.stripeCustomerId, 'cus_new'); +}); + +test('billing service creates stripe customer and inserts missing plan row', async () => { + const mock = makeDbMock(); + const billing = new BillingService(mock as any); + + (billing as any).stripe = { + customers: { + create: async () => ({ id: 'cus_inserted' }), + }, + }; + + const out = await billing.ensureStripeCustomer('u2', 'u2@example.com'); + assert.equal(out, 'cus_inserted'); + assert.equal(mock.calls.inserted.userId, 'u2'); + assert.equal(mock.calls.inserted.stripeCustomerId, 'cus_inserted'); +}); + +test('billing service creates checkout session', async () => { + const prevPrice = process.env.STRIPE_PRICE_PRO; + process.env.STRIPE_PRICE_PRO = 'price_test_123'; + + const mock = makeDbMock({ stripeCustomerId: 'cus_existing', tier: 'pro', status: 'active' }); + const billing = new BillingService(mock as any); + + (billing as any).stripe = { + checkout: { + sessions: { + create: async () => ({ id: 'cs_123', url: 'https://checkout.stripe.test/session' }), + }, + }, + }; + + const out = await billing.createCheckoutSession({ userId: 'u1', email: 'u1@example.com' }); + assert.equal(out.sessionId, 'cs_123'); + assert.ok(out.url?.includes('checkout.stripe.test')); + + process.env.STRIPE_PRICE_PRO = prevPrice; +}); + +test('billing service rejects checkout without price configuration', async () => { + const prevPrice = process.env.STRIPE_PRICE_PRO; + delete process.env.STRIPE_PRICE_PRO; + + const mock = makeDbMock({ stripeCustomerId: 'cus_existing', tier: 'free', status: 'active' }); + const billing = new BillingService(mock as any); + (billing as any).stripe = { checkout: { sessions: { create: async () => ({ id: 'x', url: 'x' }) } } }; + + await assert.rejects(() => billing.createCheckoutSession({ userId: 'u1', email: 'u1@example.com' })); + + process.env.STRIPE_PRICE_PRO = prevPrice; +}); + +test('billing service creates portal session', async () => { + const mock = makeDbMock({ stripeCustomerId: 'cus_existing', tier: 'pro', status: 'active' }); + const billing = new BillingService(mock as any); + + (billing as any).stripe = { + billingPortal: { + sessions: { + create: async () => ({ url: 'https://billing.stripe.test/portal' }), + }, + }, + }; + + const out = await billing.createBillingPortalSession({ userId: 'u1', email: 'u1@example.com' }); + assert.ok(out.url.includes('billing.stripe.test')); +}); + +test('billing service handles subscription updated webhook and upgrades plan', async () => { + const mock = makeDbMock({ stripeCustomerId: 'cus_existing', tier: 'free', status: 'active' }); + const billing = new BillingService(mock as any); + + const event: any = { + type: 'customer.subscription.updated', + data: { + object: { + id: 'sub_123', + customer: 'cus_existing', + status: 'active', + metadata: { userId: 'u1' }, + }, + }, + }; + + const out = await billing.handleWebhook(event); + assert.equal(out.received, true); + assert.equal(mock.calls.updated.tier, 'pro'); + assert.equal(mock.calls.updated.stripeSubscriptionId, 'sub_123'); +}); + +test('billing service handles subscription deleted webhook and downgrades plan', async () => { + const mock = makeDbMock({ stripeCustomerId: 'cus_existing', tier: 'pro', status: 'active' }); + const billing = new BillingService(mock as any); + + const event: any = { + type: 'customer.subscription.deleted', + data: { + object: { + id: 'sub_123', + customer: 'cus_existing', + status: 'canceled', + metadata: { userId: 'u1' }, + }, + }, + }; + + await billing.handleWebhook(event); + assert.equal(mock.calls.updated.tier, 'free'); + assert.equal(mock.calls.updated.status, 'canceled'); +}); + +test('billing service ignores unrelated webhook event types', async () => { + const mock = makeDbMock({ stripeCustomerId: 'cus_existing', tier: 'free', status: 'active' }); + const billing = new BillingService(mock as any); + + const event: any = { + type: 'invoice.paid', + data: { + object: { id: 'in_123' }, + }, + }; + + const out = await billing.handleWebhook(event); + assert.equal(out.received, true); + assert.equal(mock.calls.updated, null); + assert.equal(mock.calls.inserted, null); +}); + +test('billing service validates webhook signature when secret is configured', async () => { + const prevSecret = process.env.STRIPE_WEBHOOK_SECRET; + process.env.STRIPE_WEBHOOK_SECRET = 'whsec_test_123'; + + const mock = makeDbMock({ stripeCustomerId: 'cus_existing', tier: 'free', status: 'active' }); + const billing = new BillingService(mock as any); + + let called = false; + (billing as any).stripe = { + webhooks: { + constructEvent: () => { + called = true; + return { + type: 'invoice.paid', + data: { object: { id: 'in_123' } }, + }; + }, + }, + }; + + const out = await billing.handleWebhook({ any: 'payload' }, 'sig_test_123'); + assert.equal(out.received, true); + assert.equal(called, true); + assert.equal(mock.calls.updated, null); + + process.env.STRIPE_WEBHOOK_SECRET = prevSecret; +}); + +test('billing service does not require signature when webhook secret is set but signature missing', async () => { + const prevSecret = process.env.STRIPE_WEBHOOK_SECRET; + process.env.STRIPE_WEBHOOK_SECRET = 'whsec_test_123'; + + const mock = makeDbMock({ stripeCustomerId: 'cus_existing', tier: 'free', status: 'active' }); + const billing = new BillingService(mock as any); + + let called = false; + (billing as any).stripe = { + webhooks: { + constructEvent: () => { + called = true; + return { type: 'invoice.paid', data: { object: { id: 'in_123' } } }; + }, + }, + }; + + const out = await billing.handleWebhook({ type: 'invoice.paid', data: { object: { id: 'in_123' } } }); + assert.equal(out.received, true); + assert.equal(called, false); + assert.equal(mock.calls.updated, null); + + process.env.STRIPE_WEBHOOK_SECRET = prevSecret; +}); diff --git a/test/chess.engine.test.ts b/test/chess.engine.test.ts new file mode 100644 index 0000000..4974659 --- /dev/null +++ b/test/chess.engine.test.ts @@ -0,0 +1,30 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createState, move, legalMoves, resign } from '../src/modules/chess/chess.engine.js'; + +test('create state and perform legal move', () => { + const state = createState({ mode: 'pvp' }); + const res = move(state, { from: 'e2', to: 'e4' }); + assert.equal('error' in res, false); + assert.equal(state.chess.turn(), 'b'); +}); + +test('illegal move is rejected', () => { + const state = createState({ mode: 'pvp' }); + const res = move(state, { from: 'e2', to: 'e5' }); + assert.equal('error' in res, true); +}); + +test('legal moves from e2 include e4 at start', () => { + const state = createState({ mode: 'pvp' }); + const moves = legalMoves(state, 'e2'); + assert.ok(moves.includes('e4')); +}); + +test('resign ends game', () => { + const state = createState({ mode: 'pvp' }); + resign(state, 'w'); + assert.equal(state.status, 'finished'); + assert.equal(state.result?.reason, 'resign'); + assert.equal(state.result?.winner, 'b'); +}); diff --git a/test/chess.module.test.ts b/test/chess.module.test.ts new file mode 100644 index 0000000..c861d12 --- /dev/null +++ b/test/chess.module.test.ts @@ -0,0 +1,135 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { ChessService } from '../src/modules/chess/chess.service.js'; +import { createState, serialize } from '../src/modules/chess/chess.engine.js'; + +function makeDbMock(row?: any) { + const state: any = { + gameRow: row || null, + players: [] as any[], + gamePlayers: [] as any[], + }; + + const db: any = { + insert: () => ({ + values: async (v: any) => { + if (v.displayName) { + state.players.push(v); + } else if (v.gameId && v.playerId) { + state.gamePlayers.push(v); + } else if (v.id && v.userId && v.fen) { + state.gameRow = v; + } + }, + }), + delete: () => ({ + where: async () => { + state.players = []; + }, + }), + update: () => ({ + set: (v: any) => ({ + where: async () => { + if (v.playerId && state.gamePlayers.length) { + state.gamePlayers[0] = { ...state.gamePlayers[0], ...v }; + } + if (state.players.length && (v.displayName !== undefined || v.rating !== undefined || v.externalAppUserId !== undefined)) { + state.players[0] = { ...state.players[0], ...v }; + } + if (state.gameRow) state.gameRow = { ...state.gameRow, ...v }; + }, + }), + }), + select: () => ({ + from: (table: any) => ({ + where: () => ({ + limit: async (n?: number) => { + const name = table?.[Symbol.for('drizzle:Name')] || table?._?.name || ''; + if (name === 'games') return state.gameRow ? [state.gameRow].slice(0, n ?? 1) : []; + if (name === 'players') return state.players.slice(0, n ?? state.players.length); + if (name === 'game_players') return state.gamePlayers.slice(0, n ?? state.gamePlayers.length); + return []; + }, + orderBy: () => ({ + limit: () => ({ + offset: async () => (state.players.length ? state.players : state.gameRow ? [state.gameRow] : []), + }), + }), + offset: async () => (state.players.length ? state.players : []), + }), + }), + }), + }; + return { db, state }; +} + +test('chess service create returns game id', async () => { + const svc = new ChessService(makeDbMock() as any); + const game = await svc.create('u1', { mode: 'pvp' }); + assert.equal(typeof game.id, 'string'); +}); + +test('chess service move flow works', async () => { + const state = createState({ mode: 'pvp' }); + const row = { id: 'g1', userId: 'u1', ...serialize(state) }; + const svc = new ChessService(makeDbMock(row) as any); + const out: any = await svc.makeMove('u1', 'g1', { from: 'e2', to: 'e4' }); + assert.equal(out.move.san, 'e4'); +}); + +test('chess service list returns paginated items', async () => { + const state = createState({ mode: 'pvp' }); + const row = { id: 'g1', userId: 'u1', ...serialize(state) }; + const svc = new ChessService(makeDbMock(row) as any); + const out: any = await svc.list('u1', { page: 1, limit: 20 }); + assert.equal(out.page, 1); + assert.equal(out.limit, 20); + assert.equal(out.items.length, 1); +}); + +test('chess service creates and lists players', async () => { + const mock = makeDbMock(); + const svc = new ChessService(mock as any); + await svc.createPlayer('u1', { displayName: 'Magnus', rating: 2850, externalAppUserId: 'ext-1' }); + const out: any = await svc.listPlayers('u1', { page: 1, limit: 20 }); + assert.equal(out.items.length, 1); + assert.equal(out.items[0].displayName, 'Magnus'); +}); + +test('chess service assigns player to game color', async () => { + const gameState = createState({ mode: 'pvp' }); + const row = { id: 'g1', userId: 'u1', ...serialize(gameState) }; + const mock = makeDbMock(row); + mock.state.players.push({ id: 'p1', userId: 'u1', displayName: 'Player 1', rating: 1200, updatedAt: new Date(), createdAt: new Date() }); + + const svc = new ChessService(mock as any); + const out: any = await svc.assignPlayerToGame('u1', 'g1', { playerId: 'p1', color: 'w' }); + assert.equal(out.ok, true); +}); + +test('chess service updates owned player', async () => { + const mock = makeDbMock(); + mock.state.players.push({ id: 'p1', userId: 'u1', displayName: 'Old', rating: 1200, externalAppUserId: null, updatedAt: new Date(), createdAt: new Date() }); + const svc = new ChessService(mock as any); + const out: any = await svc.updatePlayer('u1', 'p1', { displayName: 'New Name', rating: 1300 }); + assert.equal(out.displayName, 'New Name'); + assert.equal(out.rating, 1300); +}); + +test('chess service deletes owned player', async () => { + const mock = makeDbMock(); + mock.state.players.push({ id: 'p1', userId: 'u1', displayName: 'Delete Me', rating: 1200, updatedAt: new Date(), createdAt: new Date() }); + const svc = new ChessService(mock as any); + await svc.deletePlayer('u1', 'p1'); + const listed: any = await svc.listPlayers('u1', { page: 1, limit: 20 }); + assert.equal(listed.items.length, 0); +}); + +test('chess service rejects assigning missing player to game', async () => { + const gameState = createState({ mode: 'pvp' }); + const row = { id: 'g1', userId: 'u1', ...serialize(gameState) }; + const mock = makeDbMock(row); + + const svc = new ChessService(mock as any); + await assert.rejects(() => svc.assignPlayerToGame('u1', 'g1', { playerId: 'missing-player', color: 'w' })); +}); diff --git a/test/env.config.test.ts b/test/env.config.test.ts new file mode 100644 index 0000000..9ad4908 --- /dev/null +++ b/test/env.config.test.ts @@ -0,0 +1,49 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { loadEnv } from '../src/config/env.js' + +test('loadEnv parses valid env and defaults APP_URL/PORT', () => { + const env = loadEnv({ + DATABASE_URL: 'https://db.example.com', + GOOGLE_CLIENT_ID: 'google-client-id-123456', + } as any) + + assert.equal(env.PORT, 3000) + assert.equal(env.APP_URL, 'http://localhost:3001') +}) + +test('loadEnv throws for invalid DATABASE_URL', () => { + assert.throws(() => + loadEnv({ + DATABASE_URL: 'not-a-url', + GOOGLE_CLIENT_ID: 'google-client-id-123456', + APP_URL: 'http://localhost:3001', + } as any), + ) +}) + +test('loadEnv parses optional public app url and stripe vars', () => { + const env = loadEnv({ + DATABASE_URL: 'https://db.example.com', + GOOGLE_CLIENT_ID: 'google-client-id-123456', + NEXT_PUBLIC_APP_URL: 'https://app.example.com', + STRIPE_SECRET_KEY: 'sk_test_123', + STRIPE_WEBHOOK_SECRET: 'whsec_123', + STRIPE_PRICE_PRO: 'price_123', + } as any) + + assert.equal(env.NEXT_PUBLIC_APP_URL, 'https://app.example.com') + assert.equal(env.STRIPE_SECRET_KEY, 'sk_test_123') + assert.equal(env.STRIPE_WEBHOOK_SECRET, 'whsec_123') + assert.equal(env.STRIPE_PRICE_PRO, 'price_123') +}) + +test('loadEnv throws for invalid APP_URL', () => { + assert.throws(() => + loadEnv({ + DATABASE_URL: 'https://db.example.com', + GOOGLE_CLIENT_ID: 'google-client-id-123456', + APP_URL: 'not-a-url', + } as any), + ) +}) diff --git a/test/health.module.test.ts b/test/health.module.test.ts new file mode 100644 index 0000000..c3147c2 --- /dev/null +++ b/test/health.module.test.ts @@ -0,0 +1,12 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { HealthController } from '../src/modules/health/health.controller.js'; + +test('health root and api shape', () => { + const h = new HealthController(); + const root = h.root(); + const api = h.api(); + assert.equal(root.ok, true); + assert.equal(typeof api.message, 'string'); + assert.ok(Array.isArray(api.chess)); +}); diff --git a/test/http-exception.filter.test.ts b/test/http-exception.filter.test.ts new file mode 100644 index 0000000..fd6df63 --- /dev/null +++ b/test/http-exception.filter.test.ts @@ -0,0 +1,72 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { BadRequestException } from '@nestjs/common'; +import { HttpExceptionFilter } from '../src/common/filters/http-exception.filter.js'; + +function makeHost(url = '/games') { + const payload: any = { statusCode: null, body: null }; + const response = { + status(code: number) { + payload.statusCode = code; + return this; + }, + json(body: unknown) { + payload.body = body; + return this; + }, + }; + + const host: any = { + switchToHttp: () => ({ + getResponse: () => response, + getRequest: () => ({ url }), + }), + }; + + return { host, payload }; +} + +test('http exception filter shapes known HttpException', () => { + const filter = new HttpExceptionFilter(); + const { host, payload } = makeHost('/billing/checkout-session'); + + filter.catch(new BadRequestException('Missing priceId'), host); + + assert.equal(payload.statusCode, 400); + assert.equal((payload.body as any).error.statusCode, 400); + assert.equal((payload.body as any).error.message, 'Missing priceId'); + assert.equal((payload.body as any).path, '/billing/checkout-session'); + assert.ok((payload.body as any).timestamp); +}); + +test('http exception filter shapes unknown exception as 500', () => { + const filter = new HttpExceptionFilter(); + const { host, payload } = makeHost('/games/abc'); + + filter.catch(new Error('boom'), host); + + assert.equal(payload.statusCode, 500); + assert.equal((payload.body as any).error.message, 'Internal server error'); + assert.equal((payload.body as any).path, '/games/abc'); +}); + +test('http exception filter joins validation message arrays', () => { + const filter = new HttpExceptionFilter(); + const { host, payload } = makeHost('/games'); + + filter.catch( + new BadRequestException({ + message: ['mode must be one of: pvp, pve', 'timeControl.initialSeconds must be a positive number'], + error: 'Bad Request', + statusCode: 400, + }), + host, + ); + + assert.equal(payload.statusCode, 400); + assert.equal( + (payload.body as any).error.message, + 'mode must be one of: pvp, pve, timeControl.initialSeconds must be a positive number', + ); + assert.equal((payload.body as any).error.code, 'Bad Request'); +}); diff --git a/test/request-logging.middleware.test.ts b/test/request-logging.middleware.test.ts new file mode 100644 index 0000000..ee06247 --- /dev/null +++ b/test/request-logging.middleware.test.ts @@ -0,0 +1,76 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { buildRequestLogLine, RequestLoggingMiddleware } from '../src/common/middleware/request-logging.middleware.js'; + +test('request log line includes requestId, route, status and duration', () => { + const line = buildRequestLogLine({ + requestId: 'req_123', + method: 'POST', + url: '/games', + statusCode: 201, + durationMs: 42, + }); + + assert.equal(line, '[req_123] POST /games -> 201 42ms'); +}); + +test('request log line has safe defaults', () => { + const line = buildRequestLogLine({ requestId: 'req_456' }); + assert.equal(line, '[req_456] UNKNOWN / -> 0 0ms'); +}); + +test('request logging middleware keeps incoming x-request-id', () => { + const middleware = new RequestLoggingMiddleware(); + + const headers: Record = {}; + const listeners: Record void> = {}; + const req: any = { + headers: { 'x-request-id': 'req_incoming_12345' }, + method: 'GET', + url: '/health', + }; + const res: any = { + statusCode: 200, + setHeader: (k: string, v: string) => { + headers[k] = v; + }, + on: (event: string, cb: () => void) => { + listeners[event] = cb; + }, + }; + + let called = false; + middleware.use(req, res, () => { + called = true; + }); + + assert.equal(called, true); + assert.equal(req.requestId, 'req_incoming_12345'); + assert.equal(headers['x-request-id'], 'req_incoming_12345'); + assert.equal(typeof listeners.finish, 'function'); +}); + +test('request logging middleware generates request id when missing/invalid', () => { + const middleware = new RequestLoggingMiddleware(); + + const headers: Record = {}; + const req: any = { + headers: { 'x-request-id': 'abc' }, + method: 'GET', + url: '/health', + }; + const res: any = { + statusCode: 200, + setHeader: (k: string, v: string) => { + headers[k] = v; + }, + on: () => undefined, + }; + + middleware.use(req, res, () => undefined); + + assert.equal(typeof req.requestId, 'string'); + assert.ok(req.requestId.length > 10); + assert.equal(headers['x-request-id'], req.requestId); + assert.notEqual(req.requestId, 'abc'); +}); diff --git a/test/users.module.test.ts b/test/users.module.test.ts new file mode 100644 index 0000000..1beb6c2 --- /dev/null +++ b/test/users.module.test.ts @@ -0,0 +1,40 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { UsersService } from '../src/modules/users/users.service.js'; + +function makeDbMock() { + const calls: any = { inserted: null, updated: null }; + const db: any = { + select: () => ({ from: () => ({ where: () => ({ limit: async () => [] }) }) }), + insert: () => ({ values: async (v: any) => (calls.inserted = v) }), + update: () => ({ set: (v: any) => ({ where: async () => (calls.updated = v) }) }), + }; + return { db, calls }; +} + +test('users service creates api key', async () => { + const mock = makeDbMock(); + const users = new UsersService(mock as any); + (users as any).getPlan = async () => ({ tier: 'free', status: 'active' }); + (users as any).listApiKeys = async () => []; + + const out = await users.createApiKey('u1', 'main'); + assert.ok(out.apiKey.startsWith('chess_')); + assert.equal(mock.calls.inserted.userId, 'u1'); +}); + +test('users service enforces free plan API key limit', async () => { + const mock = makeDbMock(); + const users = new UsersService(mock as any); + (users as any).getPlan = async () => ({ tier: 'free', status: 'active' }); + (users as any).listApiKeys = async () => [{ active: true }, { active: true }]; + + await assert.rejects(() => users.createApiKey('u1', 'overflow')); +}); + +test('users service revokes api key', async () => { + const mock = makeDbMock(); + const users = new UsersService(mock as any); + await users.revokeApiKey('u1', 'k1'); + assert.equal(mock.calls.updated.active, false); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d5b7b97 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2022", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "outDir": "dist", + "sourceMap": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000..064702d --- /dev/null +++ b/web/.env.example @@ -0,0 +1,2 @@ +NEXT_PUBLIC_API_BASE_URL=http://localhost:3000 +NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..f74c781 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,2 @@ +.next +node_modules diff --git a/web/next-env.d.ts b/web/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/web/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/web/next.config.mjs b/web/next.config.mjs new file mode 100644 index 0000000..089b7c0 --- /dev/null +++ b/web/next.config.mjs @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone' +} + +export default nextConfig diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..1df4791 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,1654 @@ +{ + "name": "chess-api-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chess-api-web", + "version": "0.1.0", + "dependencies": { + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "lucide-react": "^0.460.0", + "next": "14.2.5", + "react": "18.3.1", + "react-dom": "18.3.1", + "tailwind-merge": "^2.5.2" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "@types/react": "^18.3.12", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.16", + "typescript": "^5.7.3" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz", + "integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz", + "integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz", + "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz", + "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz", + "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz", + "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz", + "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz", + "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz", + "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz", + "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001772", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001772.tgz", + "integrity": "sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg==", + "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/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lucide-react": { + "version": "0.460.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.460.0.tgz", + "integrity": "sha512-BVtq/DykVeIvRTJvRAgCsOwaGL8Un3Bxh8MbDxMhEWlZay3T4IpEKDEpwt5KZ0KJMHzgm6jrltxlT5eXOWXDHg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/next": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz", + "integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==", + "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.5", + "@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" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.5", + "@next/swc-darwin-x64": "14.2.5", + "@next/swc-linux-arm64-gnu": "14.2.5", + "@next/swc-linux-arm64-musl": "14.2.5", + "@next/swc-linux-x64-gnu": "14.2.5", + "@next/swc-linux-x64-musl": "14.2.5", + "@next/swc-win32-arm64-msvc": "14.2.5", + "@next/swc-win32-ia32-msvc": "14.2.5", + "@next/swc-win32-x64-msvc": "14.2.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "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/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "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", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", + "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "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.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "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" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..af3bc8f --- /dev/null +++ b/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "chess-api-web", + "private": true, + "version": "0.1.0", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "14.2.5", + "react": "18.3.1", + "react-dom": "18.3.1", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "tailwind-merge": "^2.5.2", + "lucide-react": "^0.460.0" + }, + "devDependencies": { + "typescript": "^5.7.3", + "@types/react": "^18.3.12", + "@types/node": "^22.10.2", + "tailwindcss": "^3.4.16", + "postcss": "^8.4.49", + "autoprefixer": "^10.4.20" + } +} \ No newline at end of file diff --git a/web/postcss.config.js b/web/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/web/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/web/src/app/dashboard/page.tsx b/web/src/app/dashboard/page.tsx new file mode 100644 index 0000000..e63c96f --- /dev/null +++ b/web/src/app/dashboard/page.tsx @@ -0,0 +1,139 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { useRouter } from 'next/navigation' +import { Nav } from '@/components/nav' +import { Card, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { clearToken, getToken } from '@/lib/auth-store' +import { createApiKey, getPlan, listApiKeys, me, revokeApiKey } from '@/lib/api' + +type ApiKey = { id: string; name: string; keyPrefix: string; active: boolean; lastUsedAt?: string | null } + +export default function DashboardPage() { + const router = useRouter() + const [user, setUser] = useState(null) + const [keys, setKeys] = useState([]) + const [newKey, setNewKey] = useState(null) + const [plan, setPlan] = useState<{ tier: string; status: string } | null>(null) + const [name, setName] = useState('default') + const [loading, setLoading] = useState(true) + const token = useMemo(() => getToken(), []) + + async function refresh() { + if (!token) return + const [u, k, p] = await Promise.all([me(token), listApiKeys(token), getPlan(token)]) + setUser(u.user) + setKeys(k.items) + setPlan(p.plan) + } + + useEffect(() => { + if (!token) { + router.push('/login') + return + } + refresh() + .catch(() => { + clearToken() + router.push('/login') + }) + .finally(() => setLoading(false)) + }, [router, token]) + + if (loading) { + return ( +
+
+ ) + } + + return ( +
+
+ ) +} diff --git a/web/src/app/docs/page.tsx b/web/src/app/docs/page.tsx new file mode 100644 index 0000000..2bcac8f --- /dev/null +++ b/web/src/app/docs/page.tsx @@ -0,0 +1,108 @@ +'use client' + +import { Nav } from '@/components/nav' +import { Card, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { API_BASE } from '@/lib/api' + +const endpoints = [ + { method: 'POST', path: '/games', desc: 'Create a game (pvp/pve, optional clock).' }, + { method: 'GET', path: '/games/:id', desc: 'Fetch current game state.' }, + { method: 'DELETE', path: '/games/:id', desc: 'Delete a game.' }, + { method: 'GET', path: '/games/:id/moves?from=e2', desc: 'List legal moves.' }, + { method: 'POST', path: '/games/:id/moves', desc: 'Submit move as SAN or from/to.' }, + { method: 'POST', path: '/games/:id/ai-move', desc: 'Engine move for pve games.' }, + { method: 'POST', path: '/games/:id/resign', desc: 'Resign as white/black.' }, +] + +function CopyButton({ value }: { value: string }) { + return ( + + ) +} + +export default function DocsPage() { + const create = `curl -X POST "${API_BASE}/games" \\ + -H "x-api-key: $API_KEY" \\ + -H "content-type: application/json" \\ + -d '{"mode":"pve","aiColor":"b","timeControl":{"initialSeconds":300,"incrementSeconds":2}}'` + + const move = `curl -X POST "${API_BASE}/games//moves" \\ + -H "x-api-key: $API_KEY" \\ + -H "content-type: application/json" \\ + -d '{"from":"e2","to":"e4"}'` + + const ai = `curl -X POST "${API_BASE}/games//ai-move" \\ + -H "x-api-key: $API_KEY"` + + return ( +
+
+ ) +} diff --git a/web/src/app/globals.css b/web/src/app/globals.css new file mode 100644 index 0000000..184e8ac --- /dev/null +++ b/web/src/app/globals.css @@ -0,0 +1,19 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: 224 45% 7%; + --foreground: 210 40% 98%; + --card: 224 38% 11%; + --card-foreground: 210 40% 98%; + --primary: 217 91% 60%; + --primary-foreground: 210 40% 98%; + --muted: 225 28% 16%; + --muted-foreground: 215 18% 70%; + --border: 224 24% 24%; +} + +body { + @apply bg-background text-foreground antialiased; +} diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx new file mode 100644 index 0000000..faea85d --- /dev/null +++ b/web/src/app/layout.tsx @@ -0,0 +1,15 @@ +import './globals.css' +import type { ReactNode } from 'react' + +export const metadata = { + title: 'Chess API', + description: 'Production-ready chess API with auth, API keys, and timed games', +} + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/web/src/app/login/page.tsx b/web/src/app/login/page.tsx new file mode 100644 index 0000000..ffaa341 --- /dev/null +++ b/web/src/app/login/page.tsx @@ -0,0 +1,81 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useRouter } from 'next/navigation' +import { Nav } from '@/components/nav' +import { Card, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { googleLogin } from '@/lib/api' +import { setToken } from '@/lib/auth-store' + +declare global { + interface Window { + google?: any + } +} + +export default function LoginPage() { + const router = useRouter() + const [error, setError] = useState(null) + const [loading, setLoading] = useState(false) + + useEffect(() => { + const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID + if (!clientId) return + + const script = document.createElement('script') + script.src = 'https://accounts.google.com/gsi/client' + script.async = true + script.defer = true + script.onload = () => { + if (!window.google) return + window.google.accounts.id.initialize({ + client_id: clientId, + callback: async (resp: { credential: string }) => { + try { + setLoading(true) + const session = await googleLogin(resp.credential) + setToken(session.accessToken) + router.push('/dashboard') + } catch (e: any) { + setError(e.message || 'Login failed') + } finally { + setLoading(false) + } + }, + }) + + window.google.accounts.id.renderButton(document.getElementById('google-btn'), { + theme: 'outline', + size: 'large', + shape: 'pill', + text: 'continue_with', + }) + } + document.body.appendChild(script) + return () => { + document.body.removeChild(script) + } + }, [router]) + + return ( +
+
+ ) +} diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx new file mode 100644 index 0000000..7816131 --- /dev/null +++ b/web/src/app/page.tsx @@ -0,0 +1,114 @@ +import { Shield, Clock3, KeyRound, Zap, CheckCircle2, Rocket, Crown, Swords } from 'lucide-react' +import Link from 'next/link' +import { Button } from '@/components/ui/button' +import { Card, CardContent } from '@/components/ui/card' +import { Nav } from '@/components/nav' + +const features = [ + { + icon: Shield, + title: 'Secure Match Infrastructure', + text: 'Google auth, scoped API keys, and strict backend guards designed for real production traffic.', + }, + { + icon: Clock3, + title: 'Tournament-Ready Clocks', + text: 'Blitz, rapid, and classical support with increment, timeout handling, and deterministic turn flow.', + }, + { + icon: Swords, + title: 'PvP + PvE in One API', + text: 'Create player-vs-player rooms or engine games from the same endpoint contracts.', + }, + { + icon: KeyRound, + title: 'SaaS API Business Model', + text: 'Built for productization: per-user keys, plan-aware limits, and clean customer onboarding.', + }, +] + +const stats = [ + { label: 'Move Validation', value: 'Legal-by-Engine' }, + { label: 'Game Modes', value: 'PvP + PvE' }, + { label: 'Timing', value: 'Increment Clocks' }, +] + +export default function HomePage() { + return ( +
+
+ ) +} diff --git a/web/src/components/nav.tsx b/web/src/components/nav.tsx new file mode 100644 index 0000000..2916724 --- /dev/null +++ b/web/src/components/nav.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link' + +const links = [ + { href: '/', label: 'Home' }, + { href: '/docs', label: 'Docs' }, + { href: '/login', label: 'Login' }, + { href: '/dashboard', label: 'Dashboard' }, +] + +export function Nav() { + return ( +
+
+ Chess API + +
+
+ ) +} diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx new file mode 100644 index 0000000..39f2fc5 --- /dev/null +++ b/web/src/components/ui/button.tsx @@ -0,0 +1,29 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + 'inline-flex items-center justify-center rounded-md text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:opacity-90', + outline: 'border border-border bg-transparent hover:bg-muted', + }, + size: { + default: 'h-10 px-4 py-2', + lg: 'h-11 px-8', + }, + }, + defaultVariants: { variant: 'default', size: 'default' }, + }, +) + +export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps {} + +const Button = React.forwardRef(({ className, variant, size, ...props }, ref) => { + return