diff --git a/.env b/.env deleted file mode 100644 index 1f2813c95..000000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -DATABASE_URL="postgresql://postgres:bugloss@localhost:5432/codeit-test?schema=public" \ No newline at end of file diff --git a/part4-mission11/.dockerignore b/part4-mission11/.dockerignore new file mode 100644 index 000000000..aeea18e02 --- /dev/null +++ b/part4-mission11/.dockerignore @@ -0,0 +1,18 @@ +# 빌드/런타임에 필요 없는 것들 + +coverage +node_modules +dist +infra + +# VCS / 설정 / 도구 파일 +.git +.gitignore +Dockerfile +docker-compose.yml + +# 로그 + +npm-debug.log +*.log + diff --git a/part4-mission11/.env.sample b/part4-mission11/.env.sample new file mode 100644 index 000000000..9d2ea8292 --- /dev/null +++ b/part4-mission11/.env.sample @@ -0,0 +1,20 @@ +NODE_ENV=production + +# RDS용 +DATABASE_URL="postgresql://appuser:비밀번호@rds-endpoint:5432/appdb?schema=public" + +# JWT +ACCESS_TOKEN_SECRET="prod_secret_access" +REFRESH_TOKEN_SECRET="prod_secret_refresh" + +# Express 내부 포트 (nginx에서 80 → 여기로 프록시) +PORT=4000 + +# 프론트 배포 주소 +CORS_ORIGIN=https://내-프론트-도메인 + +# S3 +AWS_REGION=ap-northeast-2 +AWS_S3_BUCKET=내-버킷-이름 +AWS_ACCESS_KEY_ID=발급받은키 +AWS_SECRET_ACCESS_KEY=발급받은시크릿 \ No newline at end of file diff --git a/part4-mission11/.github/PULL_REQUEST_TEMPLATE.md b/part4-mission11/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..ec85f6f1a --- /dev/null +++ b/part4-mission11/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ + +## 요구사항 + +### 기본 +- [x] 기본 항목 1 +- [ ] 기본 항목 2 + +### 심화 +- [ ] 심화 항목 1 +- [ ] 심화 항목 2 + +## 주요 변경사항 +- +- + +## 스크린샷 +![image](이미지url) + +## 멘토에게 +- 셀프 코드 리뷰를 통해 질문 이어가겠습니다. +- diff --git a/part4-mission11/.github/workflows/main-cicd.yml b/part4-mission11/.github/workflows/main-cicd.yml new file mode 100644 index 000000000..5aee17219 --- /dev/null +++ b/part4-mission11/.github/workflows/main-cicd.yml @@ -0,0 +1,43 @@ +name: Main CI-CD + +on: + push: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Use Node + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + deploy: + needs: test + runs-on: ubuntu-latest + + steps: + - name: Deploy to EC2 via SSH + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + script: | + cd /home/ec2-user/app + git pull origin main + npm ci --only=production --no-audit --no-fund + npm run build + pm2 restart mission11 || pm2 start dist/server.js --name mission11 diff --git a/part4-mission11/.github/workflows/pr.yml b/part4-mission11/.github/workflows/pr.yml new file mode 100644 index 000000000..cfc6235e2 --- /dev/null +++ b/part4-mission11/.github/workflows/pr.yml @@ -0,0 +1,25 @@ +name: PR + +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Use Node + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test diff --git a/part4-mission11/.gitignore b/part4-mission11/.gitignore new file mode 100644 index 000000000..fdf5a8fdb --- /dev/null +++ b/part4-mission11/.gitignore @@ -0,0 +1,28 @@ +# Node +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Uploads +src/uploads/* +!src/uploads/.gitkeep + +# Prisma generated +generated/ + +# Env +.env +.env.production + +# OS +.DS_Store + +# Build output +dist/ + +# Test coverage outputs! +coverage/ +*.lcov +lcov.info +.nyc_output/ diff --git a/part4-mission11/Dockerfile b/part4-mission11/Dockerfile new file mode 100644 index 000000000..82c095d8a --- /dev/null +++ b/part4-mission11/Dockerfile @@ -0,0 +1,11 @@ +FROM node:18 + +COPY . /app + +WORKDIR /app + +RUN npm ci && npm run build + +ENV PORT=3000 + +ENTRYPOINT [ "npm", "run", "start" ] \ No newline at end of file diff --git a/part4-mission11/README.md b/part4-mission11/README.md new file mode 100644 index 000000000..54bef1080 --- /dev/null +++ b/part4-mission11/README.md @@ -0,0 +1,28 @@ +# 패키지 설치 + +npm install + +# DB 마이그레이션 및 시딩 + +npm run db:reset + +# 개발 서버 실행 + +npm run dev + +# 빌드 후 실행 + +npm run build +npm start + +# .env 파일 필요. + +.env.example 참고해서 .env를 만들어주세요. + +# 주요 기능 + +회원가입 / 로그인 (JWT + 쿠키 기반) +게시글 / 상품 CRUD +댓글 CRUD +좋아요 (게시글/상품/댓글) +페이지네이션 & 검색 diff --git a/part4-mission11/docker-compose.yml b/part4-mission11/docker-compose.yml new file mode 100644 index 000000000..04a9f4e1a --- /dev/null +++ b/part4-mission11/docker-compose.yml @@ -0,0 +1,33 @@ +version: '3.8' + +services: + db: + image: postgres:16 + container_name: codeit-postgres + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: bugloss + POSTGRES_DB: codeit-test + ports: + - '5432:5432' + volumes: + - db-data:/var/lib/postgresql/data + + app: + build: . + container_name: codeit-express + restart: always + depends_on: + - db + environment: + NODE_ENV: development + PORT: 3000 + DATABASE_URL: postgresql://postgres:bugloss@db:5432/codeit-test?schema=public + volumes: + - ./uploads:/app/dist/uploads + ports: + - '3000:3000' + +volumes: + db-data: diff --git a/part4-mission11/eslint.config.cjs b/part4-mission11/eslint.config.cjs new file mode 100644 index 000000000..8c996acbc --- /dev/null +++ b/part4-mission11/eslint.config.cjs @@ -0,0 +1,92 @@ +const importPlugin = require('eslint-plugin-import'); +const jestPlugin = require('eslint-plugin-jest'); +const globals = require('globals'); +const tseslint = require('typescript-eslint'); + +module.exports = [ + // 예외(무시) 경로 + { ignores: ['node_modules/**', 'dist/**', 'coverage/**', 'generated/**'] }, + + // 기본 규칙 세트 + { + files: ['**/*.{ts,tsx,js,cjs}'], + languageOptions: { + parser: tseslint.parser, + ecmaVersion: 'latest', + sourceType: 'module', + globals: { ...globals.node, ...globals.es2022 }, + parserOptions: { + project: false, + }, + }, + plugins: { + '@typescript-eslint': tseslint.plugin, + import: importPlugin, + jest: jestPlugin, + }, + settings: { + 'import/resolver': { typescript: {} }, + }, + rules: { + // @typescript-eslint 권장 규칙 기반 + ...tseslint.configs.recommended.rules, + '@typescript-eslint/no-explicit-any': 'error', + + // 사용 규칙 커스터마이즈 + 'no-console': ['warn', { allow: ['warn', 'error'] }], + 'import/no-unresolved': 'off', + 'import/order': [ + 'warn', + { + 'newlines-between': 'always', + alphabetize: { order: 'asc', caseInsensitive: true }, + groups: [ + ['builtin', 'external'], + ['internal', 'parent', 'sibling', 'index'], + ], + }, + ], + + // 기본 no-unused-vars는 끄고 TS 버전만 사용 + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + ignoreRestSiblings: true, + }, + ], + }, + }, + + // 테스트 전용 오버라이드 (Jest 전역/규칙) + { + files: ['**/src/tests/**/*', '**/*.test.{ts,tsx,js}'], + languageOptions: { + globals: { ...globals.node, ...globals.jest }, + }, + rules: { + 'no-console': 'off', // 테스트 로그 허용 + }, + }, + + // CommonJS 설정 파일(.cjs) 허용 + { + files: ['**/*.cjs'], + languageOptions: { sourceType: 'script' }, + }, + + { + files: ['**/*.d.ts'], + rules: { + '@typescript-eslint/no-unused-vars': 'off', + }, + }, + + { + files: ['src/prisma/seed.ts', 'src/middlewares/logger.ts', 'src/app.ts'], + rules: { 'no-console': 'off' }, + }, +]; diff --git a/part4-mission11/infra/ec2/ecosystem.config.js b/part4-mission11/infra/ec2/ecosystem.config.js new file mode 100644 index 000000000..610a35862 --- /dev/null +++ b/part4-mission11/infra/ec2/ecosystem.config.js @@ -0,0 +1,18 @@ +module.exports = { + apps: [ + { + name: 'part4-mission10', + script: 'dist/server.js', + instances: 1, + exec_mode: 'fork', + watch: false, + env: { + NODE_ENV: 'development', + }, + env_production: { + NODE_ENV: 'production', + }, + max_memory_restart: '300M', + }, + ], +}; diff --git a/part4-mission11/infra/ec2/nginx.conf b/part4-mission11/infra/ec2/nginx.conf new file mode 100644 index 000000000..42b36a339 --- /dev/null +++ b/part4-mission11/infra/ec2/nginx.conf @@ -0,0 +1,36 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log; +pid /run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + keepalive_timeout 65; + + upstream backend { + server 127.0.0.1:4000; + } + + server { + listen 80; + server_name _; + + location / { + proxy_pass http://backend; + proxy_http_version 1.1; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + } +} diff --git a/part4-mission11/infra/ec2/secure-group-inbound.png b/part4-mission11/infra/ec2/secure-group-inbound.png new file mode 100644 index 000000000..b49e650e5 Binary files /dev/null and b/part4-mission11/infra/ec2/secure-group-inbound.png differ diff --git a/part4-mission11/infra/ec2/secure-group-outbound.png b/part4-mission11/infra/ec2/secure-group-outbound.png new file mode 100644 index 000000000..e064a8ae2 Binary files /dev/null and b/part4-mission11/infra/ec2/secure-group-outbound.png differ diff --git a/part4-mission11/infra/ec2/start.sh b/part4-mission11/infra/ec2/start.sh new file mode 100644 index 000000000..d69060b20 --- /dev/null +++ b/part4-mission11/infra/ec2/start.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -e + +echo "🚀 Starting backend server (part4-mission10)..." + +# 1. 프로젝트 디렉토리로 이동 +cd /home/ec2-user/4-sprint-mission/part4-mission10 + +# 2. pm2로 서버 실행 (dist/server.js 기준) +pm2 start dist/server.js --name part4-mission10 + +# 3. pm2 상태 저장 (재부팅 후 자동 복원) +pm2 save + +echo "✅ Server started with pm2 (part4-mission10)" diff --git a/part4-mission11/infra/rds/secure-group-inbound.png b/part4-mission11/infra/rds/secure-group-inbound.png new file mode 100644 index 000000000..020b7d21e Binary files /dev/null and b/part4-mission11/infra/rds/secure-group-inbound.png differ diff --git a/part4-mission11/infra/rds/secure-group-outbound.png b/part4-mission11/infra/rds/secure-group-outbound.png new file mode 100644 index 000000000..56b1491fd Binary files /dev/null and b/part4-mission11/infra/rds/secure-group-outbound.png differ diff --git a/part4-mission11/infra/s3/policy.png b/part4-mission11/infra/s3/policy.png new file mode 100644 index 000000000..e16a94b87 Binary files /dev/null and b/part4-mission11/infra/s3/policy.png differ diff --git a/part4-mission11/infra/s3/policy.png:Zone.Identifier b/part4-mission11/infra/s3/policy.png:Zone.Identifier new file mode 100644 index 000000000..d6c1ec682 Binary files /dev/null and b/part4-mission11/infra/s3/policy.png:Zone.Identifier differ diff --git a/part4-mission11/jest.base.cjs b/part4-mission11/jest.base.cjs new file mode 100644 index 000000000..96850f418 --- /dev/null +++ b/part4-mission11/jest.base.cjs @@ -0,0 +1,37 @@ +module.exports = { + rootDir: __dirname, + testEnvironment: 'node', + transform: { + '^.+\\.[tj]sx?$': [ + '@swc/jest', + { + jsc: { parser: { syntax: 'typescript', tsx: false }, target: 'es2020' }, + module: { type: 'es6' }, + }, + ], + }, + extensionsToTreatAsEsm: ['.ts'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + collectCoverageFrom: [ + '**/*.ts', + '!**/app.ts', + '!**/swagger.ts', + '!**/ws.ts', + '!tests/**', + '!tests/_helper/**', + ], + coveragePathIgnorePatterns: [ + '/node_modules/', + '/dist/', + '/tests/', + '/tests/_helper/', + ], + testPathIgnorePatterns: [ + '/node_modules/', + '/dist/', + '/tests/_helper/', + ], +}; diff --git a/part4-mission11/jest.config.cjs b/part4-mission11/jest.config.cjs new file mode 100644 index 000000000..a3be762f5 --- /dev/null +++ b/part4-mission11/jest.config.cjs @@ -0,0 +1,7 @@ +/** @type {import('jest').Config} */ +module.exports = { + projects: ['/jest.unit.config.cjs', '/jest.int.config.cjs'], + collectCoverage: true, + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], +}; diff --git a/part4-mission11/jest.int.config.cjs b/part4-mission11/jest.int.config.cjs new file mode 100644 index 000000000..decd38f69 --- /dev/null +++ b/part4-mission11/jest.int.config.cjs @@ -0,0 +1,20 @@ +const base = require('./jest.base.cjs'); + +/** @type {import('jest').Config} */ +module.exports = { + ...base, + rootDir: __dirname, + displayName: 'integration', + testMatch: ['**/tests/int/**/*.test.ts'], + moduleNameMapper: { + '^@/(?:lib/)?prismaClient(?:\\.(?:js|ts))?$': + '/tests/_helper/prisma-mock.ts', + '^.+/(?:lib/)?prismaClient(?:\\.(?:js|ts))?$': + '/tests/_helper/prisma-mock.ts', + + '^@/(.*)$': '/$1', + + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + setupFiles: ['/tests/_helper/jest.env.setup.ts'], +}; diff --git a/part4-mission11/jest.setup.ts b/part4-mission11/jest.setup.ts new file mode 100644 index 000000000..fcb3afd2c --- /dev/null +++ b/part4-mission11/jest.setup.ts @@ -0,0 +1,3 @@ +import { jest } from '@jest/globals'; + +jest.setTimeout(10000); diff --git a/part4-mission11/jest.unit.config.cjs b/part4-mission11/jest.unit.config.cjs new file mode 100644 index 000000000..cbd68fbfa --- /dev/null +++ b/part4-mission11/jest.unit.config.cjs @@ -0,0 +1,27 @@ +const base = require('./jest.base.cjs'); + +/** @type {import('jest').Config} */ +module.exports = { + ...base, + rootDir: __dirname, + displayName: 'unit', + testMatch: ['**/tests/unit/**/*.test.ts'], + moduleNameMapper: { + // 1) token 모듈: 상대경로/alias 모두 커버 + '^@/lib/token(?:\\.(?:js|ts))?$': '/tests/_helper/token-mock.ts', + '^.+/lib/token(?:\\.(?:js|ts))?$': '/tests/_helper/token-mock.ts', + + // 2) prisma + '^@/(?:lib/)?prismaClient(?:\\.(?:js|ts))?$': + '/tests/_helper/prisma-mock.ts', + '^.+/(?:lib/)?prismaClient(?:\\.(?:js|ts))?$': + '/tests/_helper/prisma-mock.ts', + + // 3) alias 해석 + '^@/(.*)$': '/$1', + + // 4) 마지막에 .js 확장자 제거 규칙 + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + setupFiles: ['/tests/_helper/jest.env.setup.ts'], +}; diff --git a/part4-mission11/package-lock.json b/part4-mission11/package-lock.json new file mode 100644 index 000000000..a42397a4e --- /dev/null +++ b/part4-mission11/package-lock.json @@ -0,0 +1,12669 @@ +{ + "name": "part3-mission4", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "part3-mission4", + "version": "1.0.0", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "@aws-sdk/client-s3": "^3.931.0", + "@prisma/client": "^6.15.0", + "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^17.2.2", + "express": "^5.1.0", + "multer": "^2.0.2", + "multer-s3": "^3.0.1", + "nodemon": "^3.1.10", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", + "pg": "^8.16.3", + "socket.io": "^4.8.1", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "zod": "^4.1.5" + }, + "devDependencies": { + "@swc/core": "^1.15.0", + "@swc/jest": "^0.2.39", + "@types/bcrypt": "^6.0.0", + "@types/cookie-parser": "^1.4.9", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/jest": "^29.5.12", + "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.0.0", + "@types/multer-s3": "^3.0.3", + "@types/node": "^24.5.2", + "@types/passport": "^1.0.17", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", + "@types/supertest": "^6.0.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", + "@typescript-eslint/eslint-plugin": "^8.46.3", + "@typescript-eslint/parser": "^8.46.3", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jest": "^28.14.0", + "globals": "^16.5.0", + "jest": "^29.7.0", + "prisma": "^6.15.0", + "socket.io-client": "^4.8.1", + "supertest": "^6.3.3", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "tsx": "^4.20.5", + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.3" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.931.0.tgz", + "integrity": "sha512-p+ZSRvmylk/pNImGDvLt3lOkILOexNcYvsCjvN2TR9X8RvxvPURISVp2qdGKdwUr/zkshteg1x/30GYlcTKs5g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.931.0", + "@aws-sdk/credential-provider-node": "3.931.0", + "@aws-sdk/middleware-bucket-endpoint": "3.930.0", + "@aws-sdk/middleware-expect-continue": "3.930.0", + "@aws-sdk/middleware-flexible-checksums": "3.931.0", + "@aws-sdk/middleware-host-header": "3.930.0", + "@aws-sdk/middleware-location-constraint": "3.930.0", + "@aws-sdk/middleware-logger": "3.930.0", + "@aws-sdk/middleware-recursion-detection": "3.930.0", + "@aws-sdk/middleware-sdk-s3": "3.931.0", + "@aws-sdk/middleware-ssec": "3.930.0", + "@aws-sdk/middleware-user-agent": "3.931.0", + "@aws-sdk/region-config-resolver": "3.930.0", + "@aws-sdk/signature-v4-multi-region": "3.931.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-endpoints": "3.930.0", + "@aws-sdk/util-user-agent-browser": "3.930.0", + "@aws-sdk/util-user-agent-node": "3.931.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.2", + "@smithy/eventstream-serde-browser": "^4.2.5", + "@smithy/eventstream-serde-config-resolver": "^4.3.5", + "@smithy/eventstream-serde-node": "^4.2.5", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-blob-browser": "^4.2.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/hash-stream-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/md5-js": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.9", + "@smithy/middleware-retry": "^4.4.9", + "@smithy/middleware-serde": "^4.2.5", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.8", + "@smithy/util-defaults-mode-node": "^4.2.11", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.931.0.tgz", + "integrity": "sha512-GM/CARsIUQGEspM9VhZaftFVXnNtFNUUXjpM1ePO4CHk1J/VFvXcsQr3SHWIs0F4Ll6pvy5LpcRlWW5pK7T4aQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.931.0", + "@aws-sdk/middleware-host-header": "3.930.0", + "@aws-sdk/middleware-logger": "3.930.0", + "@aws-sdk/middleware-recursion-detection": "3.930.0", + "@aws-sdk/middleware-user-agent": "3.931.0", + "@aws-sdk/region-config-resolver": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-endpoints": "3.930.0", + "@aws-sdk/util-user-agent-browser": "3.930.0", + "@aws-sdk/util-user-agent-node": "3.931.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.2", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.9", + "@smithy/middleware-retry": "^4.4.9", + "@smithy/middleware-serde": "^4.2.5", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.8", + "@smithy/util-defaults-mode-node": "^4.2.11", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.931.0.tgz", + "integrity": "sha512-l/b6AQbto4TuXL2FIm7Z+tbVjrp0LN7ESm97Sf3nneB0vjKtB6R0TS/IySzCYMgyOC3Hxz+Ka34HJXZk9eXTFw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.2", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.931.0.tgz", + "integrity": "sha512-dTNBpkKXyBdcpEjyfgkE/EFU/0NRoukLs+Pj0S8K1Dg216J9uIijpi6CaBBN+HvnaTlEItm2tzXiJpPVI+TqHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.931.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.931.0.tgz", + "integrity": "sha512-7Ge26fhMDn51BTbHgopx5+uOl4I47k15BDzYc4YT6zyjS99uycYNCA7zB500DGTTn2HK27ZDTyAyhTKZGxRxbA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.931.0", + "@aws-sdk/types": "3.930.0", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.931.0.tgz", + "integrity": "sha512-uzicpP7IHBxvAMjwGdmeke2bGTxjsKCSW7N48zuv0t0d56hmGHfcZIK5p4ry2OBJxzScp182OUAdAEG8wuSuuA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.931.0", + "@aws-sdk/credential-provider-env": "3.931.0", + "@aws-sdk/credential-provider-http": "3.931.0", + "@aws-sdk/credential-provider-process": "3.931.0", + "@aws-sdk/credential-provider-sso": "3.931.0", + "@aws-sdk/credential-provider-web-identity": "3.931.0", + "@aws-sdk/nested-clients": "3.931.0", + "@aws-sdk/types": "3.930.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.931.0.tgz", + "integrity": "sha512-eO8mfWNHz0dyYdVfPLVzmqXaSA3agZF/XvBO9/fRU90zCb8lKlXfgUmghGW7LhDkiv2v5uuizUiag7GsKoIcJw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.931.0", + "@aws-sdk/credential-provider-http": "3.931.0", + "@aws-sdk/credential-provider-ini": "3.931.0", + "@aws-sdk/credential-provider-process": "3.931.0", + "@aws-sdk/credential-provider-sso": "3.931.0", + "@aws-sdk/credential-provider-web-identity": "3.931.0", + "@aws-sdk/types": "3.930.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.931.0.tgz", + "integrity": "sha512-8Mu9r+5BUKqmKSI/WYHl5o4GeoonEb51RmoLEqG6431Uz4Y8C6gzAT69yjOJ+MwoWQ2Os37OZLOTv7SgxyOgrQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.931.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.931.0.tgz", + "integrity": "sha512-FP31lfMgNMDG4ZDX4NUZ+uoHWn76etcG8UWEgzZb4YOPV4M8a7gwU95iD+RBaK4lV3KvwH2tu68Hmne1qQpFqQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.931.0", + "@aws-sdk/core": "3.931.0", + "@aws-sdk/token-providers": "3.931.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.931.0.tgz", + "integrity": "sha512-hfX0Buw2+ie0FBiSFMmnXfugQc9fO0KvEojnNnzhk4utlWjZobMcUprOQ/VKUueg0Kga1b1xu8gEP6g1aEh3zw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.931.0", + "@aws-sdk/nested-clients": "3.931.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/lib-storage": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.931.0.tgz", + "integrity": "sha512-CG6ZZIbsrWnYLy5avjMckwaGQ6D5gExEZoEBDzu8fPIsjBd7Nxo3FJvkbE7MuK2/E762pnpWKd9BZANkciRx3A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.9", + "@smithy/smithy-client": "^4.9.5", + "buffer": "5.6.0", + "events": "3.3.0", + "stream-browserify": "3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.931.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.930.0.tgz", + "integrity": "sha512-cnCLWeKPYgvV4yRYPFH6pWMdUByvu2cy2BAlfsPpvnm4RaVioztyvxmQj5PmVN5fvWs5w/2d6U7le8X9iye2sA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.930.0.tgz", + "integrity": "sha512-5HEQ+JU4DrLNWeY27wKg/jeVa8Suy62ivJHOSUf6e6hZdVIMx0h/kXS1fHEQNNiLu2IzSEP/bFXsKBaW7x7s0g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.931.0.tgz", + "integrity": "sha512-eYWwUKeEommCrrm0Ro6fGDwVO0x2bL3niOmSnHIlIdpu7ruzAGaphj+2MekCxaSPORzkZ3yheHUzV45D8Qj63A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.931.0", + "@aws-sdk/types": "3.930.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.930.0.tgz", + "integrity": "sha512-x30jmm3TLu7b/b+67nMyoV0NlbnCVT5DI57yDrhXAPCtdgM1KtdLWt45UcHpKOm1JsaIkmYRh2WYu7Anx4MG0g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.930.0.tgz", + "integrity": "sha512-QIGNsNUdRICog+LYqmtJ03PLze6h2KCORXUs5td/hAEjVP5DMmubhtrGg1KhWyctACluUH/E/yrD14p4pRXxwA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.930.0.tgz", + "integrity": "sha512-vh4JBWzMCBW8wREvAwoSqB2geKsZwSHTa0nSt0OMOLp2PdTYIZDi0ZiVMmpfnjcx9XbS6aSluLv9sKx4RrG46A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.930.0.tgz", + "integrity": "sha512-gv0sekNpa2MBsIhm2cjP3nmYSfI4nscx/+K9u9ybrWZBWUIC4kL2sV++bFjjUz4QxUIlvKByow3/a9ARQyCu7Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@aws/lambda-invoke-store": "^0.1.1", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.931.0.tgz", + "integrity": "sha512-uWF78ht8Wgxljn6y0cEcIWfbeTVnJ0cE1Gha9ScCqscmuBCpHuFMSd/p53w3whoDhpQL3ln9mOyY3tfST/NUQA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.931.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.18.2", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.930.0.tgz", + "integrity": "sha512-N2/SvodmaDS6h7CWfuapt3oJyn1T2CBz0CsDIiTDv9cSagXAVFjPdm2g4PFJqrNBeqdDIoYBnnta336HmamWHg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.931.0.tgz", + "integrity": "sha512-Ftd+f3+y5KNYKzLXaGknwJ9hCkFWshi5C9TLLsz+fEohWc1FvIKU7MlXTeFms2eN76TTVHuG8N2otaujl6CuHg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.931.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-endpoints": "3.930.0", + "@smithy/core": "^3.18.2", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.931.0.tgz", + "integrity": "sha512-6/dXrX2nWgiWdHxooEtmKpOErms4+79AQawEvhhxpLPpa+tixl4i/MSFgHk9sjkGv5a1/P3DbnedpZWl+2wMOg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.931.0", + "@aws-sdk/middleware-host-header": "3.930.0", + "@aws-sdk/middleware-logger": "3.930.0", + "@aws-sdk/middleware-recursion-detection": "3.930.0", + "@aws-sdk/middleware-user-agent": "3.931.0", + "@aws-sdk/region-config-resolver": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-endpoints": "3.930.0", + "@aws-sdk/util-user-agent-browser": "3.930.0", + "@aws-sdk/util-user-agent-node": "3.931.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.2", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.9", + "@smithy/middleware-retry": "^4.4.9", + "@smithy/middleware-serde": "^4.2.5", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.8", + "@smithy/util-defaults-mode-node": "^4.2.11", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.930.0.tgz", + "integrity": "sha512-KL2JZqH6aYeQssu1g1KuWsReupdfOoxD6f1as2VC+rdwYFUu4LfzMsFfXnBvvQWWqQ7rZHWOw1T+o5gJmg7Dzw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.931.0.tgz", + "integrity": "sha512-EGYYDSSk7k1xbSHtb8MfEMILf5achdNnnsYKgFk0+Oul3tPQ4xUmOt5qRP6sOO3/LQHF37gBYHUF9OSA/+uVCw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.931.0", + "@aws-sdk/types": "3.930.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.931.0.tgz", + "integrity": "sha512-dr+02X9oxqmXG0856odFJ7wAXy12pr/tq2Zg+IS0TDThFvgtvx4yChkpqmc89wGoW+Aly47JPfPUXh0IMpGzIg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.931.0", + "@aws-sdk/nested-clients": "3.931.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.930.0.tgz", + "integrity": "sha512-we/vaAgwlEFW7IeftmCLlLMw+6hFs3DzZPJw7lVHbj/5HJ0bz9gndxEsS2lQoeJ1zhiiLqAqvXxmM43s0MBg0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", + "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.930.0.tgz", + "integrity": "sha512-M2oEKBzzNAYr136RRc6uqw3aWlwCxqTP1Lawps9E1d2abRPvl1p1ztQmmXp1Ak4rv8eByIZ+yQyKQ3zPdRG5dw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-endpoints": "^3.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", + "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.930.0.tgz", + "integrity": "sha512-q6lCRm6UAe+e1LguM5E4EqM9brQlDem4XDcQ87NzEvlTW6GzmNCO0w1jS0XgCFXQHjDxjdlNFX+5sRbHijwklg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/types": "^4.9.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.931.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.931.0.tgz", + "integrity": "sha512-j5if01rt7JCGYDVXck39V7IUyKAN73vKUPzmu+jp1apU3Q0lLSTZA/HCfL2HkMUKVLE67ibjKb+NCoEg0QhujA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.931.0", + "@aws-sdk/types": "3.930.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", + "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", + "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@emnapi/core": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz", + "integrity": "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz", + "integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-30.2.0.tgz", + "integrity": "sha512-44F4l4Enf+MirJN8X/NhdGkl71k5rBYiwdVlo4HxOwbu0sHV8QKrGEedb1VUU4K3W7fBKE0HGfbn7eZm0Ti3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/create-cache-key-function/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/create-cache-key-function/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/create-cache-key-function/node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@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/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@prisma/client": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.15.0.tgz", + "integrity": "sha512-wR2LXUbOH4cL/WToatI/Y2c7uzni76oNFND7+23ypLllBmIS8e3ZHhO+nud9iXSXKFt1SoM3fTZvHawg63emZw==", + "hasInstallScript": true, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.15.0.tgz", + "integrity": "sha512-KMEoec9b2u6zX0EbSEx/dRpx1oNLjqJEBZYyK0S3TTIbZ7GEGoVyGyFRk4C72+A38cuPLbfQGQvgOD+gBErKlA==", + "devOptional": true, + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.16.12", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.15.0.tgz", + "integrity": "sha512-y7cSeLuQmyt+A3hstAs6tsuAiVXSnw9T55ra77z0nbNkA8Lcq9rNcQg6PI00by/+WnE/aMRJ/W7sZWn2cgIy1g==", + "devOptional": true + }, + "node_modules/@prisma/engines": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.15.0.tgz", + "integrity": "sha512-opITiR5ddFJ1N2iqa7mkRlohCZqVSsHhRcc29QXeldMljOf4FSellLT0J5goVb64EzRTKcIDeIsJBgmilNcKxA==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "6.15.0", + "@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb", + "@prisma/fetch-engine": "6.15.0", + "@prisma/get-platform": "6.15.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb.tgz", + "integrity": "sha512-a/46aK5j6L3ePwilZYEgYDPrhBQ/n4gYjLxT5YncUTJJNRnTCVjPF86QdzUOLRdYjCLfhtZp9aum90W0J+trrg==", + "devOptional": true + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.15.0.tgz", + "integrity": "sha512-xcT5f6b+OWBq6vTUnRCc7qL+Im570CtwvgSj+0MTSGA1o9UDSKZ/WANvwtiRXdbYWECpyC3CukoG3A04VTAPHw==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "6.15.0", + "@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb", + "@prisma/get-platform": "6.15.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.15.0.tgz", + "integrity": "sha512-Jbb+Xbxyp05NSR1x2epabetHiXvpO8tdN2YNoWoA/ZsbYyxxu/CO/ROBauIFuMXs3Ti+W7N7SJtWsHGaWte9Rg==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "6.15.0" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", + "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", + "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz", + "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", + "integrity": "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.3.tgz", + "integrity": "sha512-qqpNskkbHOSfrbFbjhYj5o8VMXO26fvN1K/+HbCzUNlTuxgNcPRouUDNm+7D6CkN244WG7aK533Ne18UtJEgAA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", + "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.5.tgz", + "integrity": "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.9.0", + "@smithy/util-hex-encoding": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.5.tgz", + "integrity": "sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.5.tgz", + "integrity": "sha512-ibjQjM7wEXtECiT6my1xfiMH9IcEczMOS6xiCQXoUIYSj5b1CpBbJ3VYbdwDy8Vcg5JHN7eFpOCGk8nyZAltNQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.5.tgz", + "integrity": "sha512-+elOuaYx6F2H6x1/5BQP5ugv12nfJl66GhxON8+dWVUEDJ9jah/A0tayVdkLRP0AeSac0inYkDz5qBFKfVp2Gg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.5.tgz", + "integrity": "sha512-G9WSqbST45bmIFaeNuP/EnC19Rhp54CcVdX9PDL1zyEB514WsDVXhlyihKlGXnRycmHNmVv88Bvvt4EYxWef/Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", + "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.6.tgz", + "integrity": "sha512-8P//tA8DVPk+3XURk2rwcKgYwFvwGwmJH/wJqQiSKwXZtf/LiZK+hbUZmPj/9KzM+OVSwe4o85KTp5x9DUZTjw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.0", + "@smithy/chunked-blob-reader-native": "^4.2.1", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.5.tgz", + "integrity": "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.5.tgz", + "integrity": "sha512-6+do24VnEyvWcGdHXomlpd0m8bfZePpUKBy7m311n+JuRwug8J4dCanJdTymx//8mi0nlkflZBvJe+dEO/O12Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.5.tgz", + "integrity": "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.5.tgz", + "integrity": "sha512-Bt6jpSTMWfjCtC0s79gZ/WZ1w90grfmopVOWqkI2ovhjpD5Q2XRXuecIPB9689L2+cCySMbaXDhBPU56FKNDNg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.5.tgz", + "integrity": "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.10.tgz", + "integrity": "sha512-SoAag3QnWBFoXjwa1jenEThkzJYClidZUyqsLKwWZ8kOlZBwehrLBp4ygVDjNEM2a2AamCQ2FBA/HuzKJ/LiTA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.18.3", + "@smithy/middleware-serde": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-middleware": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.10.tgz", + "integrity": "sha512-6fOwX34gXxcqKa3bsG0mR0arc2Cw4ddOS6tp3RgUD2yoTrDTbQ2aVADnDjhUuxaiDZN2iilxndgGDhnpL/XvJA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/service-error-classification": "^4.2.5", + "@smithy/smithy-client": "^4.9.6", + "@smithy/types": "^4.9.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.5.tgz", + "integrity": "sha512-La1ldWTJTZ5NqQyPqnCNeH9B+zjFhrNoQIL1jTh4zuqXRlmXhxYHhMtI1/92OlnoAtp6JoN7kzuwhWoXrBwPqg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.5.tgz", + "integrity": "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.5.tgz", + "integrity": "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", + "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.5.tgz", + "integrity": "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.5.tgz", + "integrity": "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", + "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", + "integrity": "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.5.tgz", + "integrity": "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.0.tgz", + "integrity": "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", + "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.6.tgz", + "integrity": "sha512-hGz42hggqReicRRZUvrKDQiAmoJnx1Q+XfAJnYAGu544gOfxQCAC3hGGD7+Px2gEUUxB/kKtQV7LOtBRNyxteQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.18.3", + "@smithy/middleware-endpoint": "^4.3.10", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", + "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.5.tgz", + "integrity": "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.9.tgz", + "integrity": "sha512-Bh5bU40BgdkXE2BcaNazhNtEXi1TC0S+1d84vUwv5srWfvbeRNUKFzwKQgC6p6MXPvEgw+9+HdX3pOwT6ut5aw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.6", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.12.tgz", + "integrity": "sha512-EHZwe1E9Q7umImIyCKQg/Cm+S+7rjXxCRvfGmKifqwYvn7M8M4ZcowwUOQzvuuxUUmdzCkqL0Eq0z1m74Pq6pw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.3", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.6", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.5.tgz", + "integrity": "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.5.tgz", + "integrity": "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.5.tgz", + "integrity": "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.6.tgz", + "integrity": "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.5.tgz", + "integrity": "sha512-Dbun99A3InifQdIrsXZ+QLcC0PGBPAdrl4cj1mTgJvyc9N2zf7QSxg8TBkzsCmGJdE3TLbO9ycwpY0EkWahQ/g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "devOptional": true + }, + "node_modules/@swc/core": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.0.tgz", + "integrity": "sha512-8SnJV+JV0rYbfSiEiUvYOmf62E7QwsEG+aZueqSlKoxFt0pw333+bgZSQXGUV6etXU88nxur0afVMaINujBMSw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.0", + "@swc/core-darwin-x64": "1.15.0", + "@swc/core-linux-arm-gnueabihf": "1.15.0", + "@swc/core-linux-arm64-gnu": "1.15.0", + "@swc/core-linux-arm64-musl": "1.15.0", + "@swc/core-linux-x64-gnu": "1.15.0", + "@swc/core-linux-x64-musl": "1.15.0", + "@swc/core-win32-arm64-msvc": "1.15.0", + "@swc/core-win32-ia32-msvc": "1.15.0", + "@swc/core-win32-x64-msvc": "1.15.0" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.0.tgz", + "integrity": "sha512-TBKWkbnShnEjlIbO4/gfsrIgAqHBVqgPWLbWmPdZ80bF393yJcLgkrb7bZEnJs6FCbSSuGwZv2rx1jDR2zo6YA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.0.tgz", + "integrity": "sha512-f5JKL1v1H56CIZc1pVn4RGPOfnWqPwmuHdpf4wesvXunF1Bx85YgcspW5YxwqG5J9g3nPU610UFuExJXVUzOiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.0.tgz", + "integrity": "sha512-duK6nG+WyuunnfsfiTUQdzC9Fk8cyDLqT9zyXvY2i2YgDu5+BH5W6wM5O4mDNCU5MocyB/SuF5YDF7XySnowiQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.0.tgz", + "integrity": "sha512-ITe9iDtTRXM98B91rvyPP6qDVbhUBnmA/j4UxrHlMQ0RlwpqTjfZYZkD0uclOxSZ6qIrOj/X5CaoJlDUuQ0+Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.0.tgz", + "integrity": "sha512-Q5ldc2bzriuzYEoAuqJ9Vr3FyZhakk5hiwDbniZ8tlEXpbjBhbOleGf9/gkhLaouDnkNUEazFW9mtqwUTRdh7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.0.tgz", + "integrity": "sha512-pY4is+jEpOxlYCSnI+7N8Oxbap9TmTz5YT84tUvRTlOlTBwFAUlWFCX0FRwWJlsfP0TxbqhIe8dNNzlsEmJbXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.0.tgz", + "integrity": "sha512-zYEt5eT8y8RUpoe7t5pjpoOdGu+/gSTExj8PV86efhj6ugB3bPlj3Y85ogdW3WMVXr4NvwqvzdaYGCZfXzSyVg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.0.tgz", + "integrity": "sha512-zC1rmOgFH5v2BCbByOazEqs0aRNpTdLRchDExfcCfgKgeaD+IdpUOqp7i3VG1YzkcnbuZjMlXfM0ugpt+CddoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.0.tgz", + "integrity": "sha512-7t9U9KwMwQblkdJIH+zX1V4q1o3o41i0HNO+VlnAHT5o+5qHJ963PHKJ/pX3P2UlZnBCY465orJuflAN4rAP9A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.0.tgz", + "integrity": "sha512-VE0Zod5vcs8iMLT64m5QS1DlTMXJFI/qSgtMDRx8rtZrnjt6/9NW8XUaiPJuRu8GluEO1hmHoyf1qlbY19gGSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/jest": { + "version": "0.2.39", + "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.39.tgz", + "integrity": "sha512-eyokjOwYd0Q8RnMHri+8/FS1HIrIUKK/sRrFp8c1dThUOfNeCWbLmBP1P5VsKdvmkd25JaH+OKYwEYiAYg9YAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/create-cache-key-function": "^30.0.0", + "@swc/counter": "^0.1.3", + "jsonc-parser": "^3.2.0" + }, + "engines": { + "npm": ">= 7.0.0" + }, + "peerDependencies": { + "@swc/core": "*" + } + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz", + "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==", + "dev": true, + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true + }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/multer-s3": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/multer-s3/-/multer-s3-3.0.3.tgz", + "integrity": "sha512-VgWygI9UwyS7loLithUUi0qAMIDWdNrERS2Sb06UuPYiLzKuIFn2NgL7satyl4v8sh/LLoU7DiPanvbQaRg9Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@aws-sdk/client-s3": "^3.0.0", + "@types/multer": "*", + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "dependencies": { + "undici-types": "~7.12.0" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-local": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.38.tgz", + "integrity": "sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", + "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "dev": true + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.34", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz", + "integrity": "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", + "integrity": "sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/type-utils": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.3", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", + "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.3.tgz", + "integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.3", + "@typescript-eslint/types": "^8.46.3", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz", + "integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz", + "integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz", + "integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.3.tgz", + "integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz", + "integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.3", + "@typescript-eslint/tsconfig-utils": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.3.tgz", + "integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz", + "integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "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" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.23", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.23.tgz", + "integrity": "sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "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==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "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.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "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==" + }, + "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==" + }, + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/c12/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "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/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001753", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz", + "integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "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/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "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/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "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" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "devOptional": true + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "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==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "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==" + }, + "node_modules/effect": { + "version": "3.16.12", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", + "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", + "devOptional": true, + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.244", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", + "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/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/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/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" + } + }, + "node_modules/engine.io/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" + } + }, + "node_modules/engine.io/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" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "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==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + }, + "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/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==" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-context": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", + "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" + }, + "peerDependencies": { + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz", + "integrity": "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==", + "dev": true, + "license": "ISC", + "dependencies": { + "debug": "^4.4.1", + "eslint-import-context": "^0.1.8", + "get-tsconfig": "^4.10.1", + "is-bun-module": "^2.0.0", + "stable-hash-x": "^0.2.0", + "tinyglobby": "^0.2.14", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^16.17.0 || >=18.6.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "28.14.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.14.0.tgz", + "integrity": "sha512-P9s/qXSMTpRTerE2FQ0qJet2gKbcGyFTPAJipoKxmWqR6uuFqIqk8FuEfg5yBieOezVrEfAMZrEwJ6yEp+1MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "engines": { + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "devOptional": true + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "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-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formidable": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", + "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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==", + "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-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "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==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-comment-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "devOptional": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead." + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead." + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "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==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "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/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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==" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "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" + } + }, + "node_modules/multer-s3": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/multer-s3/-/multer-s3-3.0.1.tgz", + "integrity": "sha512-BFwSO80a5EW4GJRBdUuSHblz2jhVSAze33ZbnGpcfEicoT0iRolx4kWR+AJV07THFRCQ78g+kelKFdjkCCaXeQ==", + "license": "MIT", + "dependencies": { + "@aws-sdk/lib-storage": "^3.46.0", + "file-type": "^3.3.0", + "html-comment-regex": "^1.1.2", + "run-parallel": "^1.1.6" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.0.0" + } + }, + "node_modules/multer/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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/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==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "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/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nypm": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz", + "integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==", + "devOptional": true, + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.2.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.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==", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "peer": true + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.15.0.tgz", + "integrity": "sha512-E6RCgOt+kUVtjtZgLQDBJ6md2tDItLJNExwI0XJeBc1FKL+Vwb+ovxXxuok9r8oBgsOXBA33fGDuE/0qDdCWqQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/config": "6.15.0", + "@prisma/engines": "6.15.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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/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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "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==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.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/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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==", + "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/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "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" + } + }, + "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==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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" + } + }, + "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==", + "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" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/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/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/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" + } + }, + "node_modules/socket.io/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" + } + }, + "node_modules/socket.io/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" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stable-hash-x": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", + "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.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/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==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/superagent": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/supertest": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.3.tgz", + "integrity": "sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==", + "deprecated": "Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^8.0.5" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "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/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.0.tgz", + "integrity": "sha512-gqs7Md3AxP4mbpXAq31o5QW+wGUZsUzVatg70yXpUR245dfIis5jAzufBd+UQM/w2xSfrhvA1eqsrgnl2PbezQ==", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "devOptional": true + }, + "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/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "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==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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.20.5", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", + "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", + "dev": true, + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.3.tgz", + "integrity": "sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.3", + "@typescript-eslint/parser": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/utils": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" + }, + "node_modules/undici-types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.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==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.20", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", + "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "dev": true, + "engines": { + "node": ">=0.4.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==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/zod": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.5.tgz", + "integrity": "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/part4-mission11/package.json b/part4-mission11/package.json new file mode 100644 index 000000000..e7f5623d6 --- /dev/null +++ b/part4-mission11/package.json @@ -0,0 +1,84 @@ +{ + "name": "sprint-mission", + "version": "1.0.0", + "description": "", + "license": "ISC", + "author": "", + "type": "module", + "main": "dist/server.js", + "scripts": { + "seed": "tsx src/prisma/seed.ts", + "postinstall": "npx prisma generate", + "db:migrate": "npx prisma migrate deploy", + "db:seed": "npx prisma db seed", + "test": "jest", + "test:unit": "jest --selectProjects unit", + "test:int": "jest --selectProjects integration", + "test:cov": "jest --coverage", + "lint": "eslint . --ext .ts,.tsx,.js,.cjs", + "lint:fix": "eslint . --ext .ts,.tsx,.js,.cjs --fix", + "dev": "tsx watch src/server.ts", + "build": "tsc", + "start": "nodemon dist/server.js", + "db:reset": "npx prisma migrate reset --skip-seed && npx prisma migrate dev && npx prisma db seed" + }, + "prisma": { + "seed": "tsx src/prisma/seed.ts" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.931.0", + "@prisma/client": "^6.15.0", + "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^17.2.2", + "express": "^5.1.0", + "multer": "^2.0.2", + "multer-s3": "^3.0.1", + "nodemon": "^3.1.10", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", + "pg": "^8.16.3", + "socket.io": "^4.8.1", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "zod": "^4.1.5" + }, + "devDependencies": { + "@swc/core": "^1.15.0", + "@swc/jest": "^0.2.39", + "@types/bcrypt": "^6.0.0", + "@types/cookie-parser": "^1.4.9", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/jest": "^29.5.12", + "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.0.0", + "@types/multer-s3": "^3.0.3", + "@types/node": "^24.5.2", + "@types/passport": "^1.0.17", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", + "@types/supertest": "^6.0.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", + "@typescript-eslint/eslint-plugin": "^8.46.3", + "@typescript-eslint/parser": "^8.46.3", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jest": "^28.14.0", + "globals": "^16.5.0", + "jest": "^29.7.0", + "prisma": "^6.15.0", + "socket.io-client": "^4.8.1", + "supertest": "^6.3.3", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "tsx": "^4.20.5", + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.3" + } +} diff --git a/part4-mission11/prisma/migrations/20250903063435_test/migration.sql b/part4-mission11/prisma/migrations/20250903063435_test/migration.sql new file mode 100644 index 000000000..d26a43ecd --- /dev/null +++ b/part4-mission11/prisma/migrations/20250903063435_test/migration.sql @@ -0,0 +1,74 @@ +-- CreateTable +CREATE TABLE "public"."User" ( + "id" SERIAL NOT NULL, + "username" TEXT NOT NULL, + "email" TEXT NOT NULL, + "images" TEXT[], + "password" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Product" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "price" INTEGER NOT NULL, + "tags" TEXT[], + "images" TEXT[], + "userId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Product_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Article" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "tags" TEXT[], + "userId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Article_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Comment" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "userId" INTEGER NOT NULL, + "articleId" INTEGER, + "productId" INTEGER, + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "public"."User"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "public"."User"("email"); + +-- AddForeignKey +ALTER TABLE "public"."Product" ADD CONSTRAINT "Product_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Article" ADD CONSTRAINT "Article_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Comment" ADD CONSTRAINT "Comment_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "public"."Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Comment" ADD CONSTRAINT "Comment_productId_fkey" FOREIGN KEY ("productId") REFERENCES "public"."Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/part4-mission11/prisma/migrations/20250904044259_test/migration.sql b/part4-mission11/prisma/migrations/20250904044259_test/migration.sql new file mode 100644 index 000000000..b8e440114 --- /dev/null +++ b/part4-mission11/prisma/migrations/20250904044259_test/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "public"."Article" ADD COLUMN "images" TEXT[]; diff --git a/part4-mission11/prisma/migrations/20250904070430_like/migration.sql b/part4-mission11/prisma/migrations/20250904070430_like/migration.sql new file mode 100644 index 000000000..5d290d994 --- /dev/null +++ b/part4-mission11/prisma/migrations/20250904070430_like/migration.sql @@ -0,0 +1,59 @@ +-- AlterTable +ALTER TABLE "public"."Article" ADD COLUMN "likeCount" INTEGER NOT NULL DEFAULT 0; + +-- AlterTable +ALTER TABLE "public"."Comment" ADD COLUMN "likeCount" INTEGER NOT NULL DEFAULT 0; + +-- AlterTable +ALTER TABLE "public"."Product" ADD COLUMN "likeCount" INTEGER NOT NULL DEFAULT 0; + +-- CreateTable +CREATE TABLE "public"."_ProductLikes" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL, + + CONSTRAINT "_ProductLikes_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateTable +CREATE TABLE "public"."_ArticleLikes" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL, + + CONSTRAINT "_ArticleLikes_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateTable +CREATE TABLE "public"."_CommentLikes" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL, + + CONSTRAINT "_CommentLikes_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateIndex +CREATE INDEX "_ProductLikes_B_index" ON "public"."_ProductLikes"("B"); + +-- CreateIndex +CREATE INDEX "_ArticleLikes_B_index" ON "public"."_ArticleLikes"("B"); + +-- CreateIndex +CREATE INDEX "_CommentLikes_B_index" ON "public"."_CommentLikes"("B"); + +-- AddForeignKey +ALTER TABLE "public"."_ProductLikes" ADD CONSTRAINT "_ProductLikes_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."_ProductLikes" ADD CONSTRAINT "_ProductLikes_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."_ArticleLikes" ADD CONSTRAINT "_ArticleLikes_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."_ArticleLikes" ADD CONSTRAINT "_ArticleLikes_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."_CommentLikes" ADD CONSTRAINT "_CommentLikes_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."_CommentLikes" ADD CONSTRAINT "_CommentLikes_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/part4-mission11/prisma/migrations/20250905090014_liked/migration.sql b/part4-mission11/prisma/migrations/20250905090014_liked/migration.sql new file mode 100644 index 000000000..f1dedd83b --- /dev/null +++ b/part4-mission11/prisma/migrations/20250905090014_liked/migration.sql @@ -0,0 +1,91 @@ +/* + Warnings: + + - You are about to drop the `_ArticleLikes` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `_CommentLikes` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `_ProductLikes` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "public"."_ArticleLikes" DROP CONSTRAINT "_ArticleLikes_A_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."_ArticleLikes" DROP CONSTRAINT "_ArticleLikes_B_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."_CommentLikes" DROP CONSTRAINT "_CommentLikes_A_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."_CommentLikes" DROP CONSTRAINT "_CommentLikes_B_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."_ProductLikes" DROP CONSTRAINT "_ProductLikes_A_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."_ProductLikes" DROP CONSTRAINT "_ProductLikes_B_fkey"; + +-- DropTable +DROP TABLE "public"."_ArticleLikes"; + +-- DropTable +DROP TABLE "public"."_CommentLikes"; + +-- DropTable +DROP TABLE "public"."_ProductLikes"; + +-- CreateTable +CREATE TABLE "public"."ProductLike" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "productId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ProductLike_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."ArticleLike" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "articleId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ArticleLike_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."CommentLike" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "commentId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "CommentLike_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "ProductLike_userId_productId_key" ON "public"."ProductLike"("userId", "productId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ArticleLike_userId_articleId_key" ON "public"."ArticleLike"("userId", "articleId"); + +-- CreateIndex +CREATE UNIQUE INDEX "CommentLike_userId_commentId_key" ON "public"."CommentLike"("userId", "commentId"); + +-- AddForeignKey +ALTER TABLE "public"."ProductLike" ADD CONSTRAINT "ProductLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."ProductLike" ADD CONSTRAINT "ProductLike_productId_fkey" FOREIGN KEY ("productId") REFERENCES "public"."Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."ArticleLike" ADD CONSTRAINT "ArticleLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."ArticleLike" ADD CONSTRAINT "ArticleLike_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "public"."Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."CommentLike" ADD CONSTRAINT "CommentLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."CommentLike" ADD CONSTRAINT "CommentLike_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "public"."Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/part4-mission11/prisma/migrations/20250909071844_ts/migration.sql b/part4-mission11/prisma/migrations/20250909071844_ts/migration.sql new file mode 100644 index 000000000..3cbedcd75 --- /dev/null +++ b/part4-mission11/prisma/migrations/20250909071844_ts/migration.sql @@ -0,0 +1,85 @@ +/* + Warnings: + + - You are about to drop the `ArticleLike` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `CommentLike` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `ProductLike` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "public"."ArticleLike" DROP CONSTRAINT "ArticleLike_articleId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."ArticleLike" DROP CONSTRAINT "ArticleLike_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."CommentLike" DROP CONSTRAINT "CommentLike_commentId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."CommentLike" DROP CONSTRAINT "CommentLike_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."ProductLike" DROP CONSTRAINT "ProductLike_productId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."ProductLike" DROP CONSTRAINT "ProductLike_userId_fkey"; + +-- DropTable +DROP TABLE "public"."ArticleLike"; + +-- DropTable +DROP TABLE "public"."CommentLike"; + +-- DropTable +DROP TABLE "public"."ProductLike"; + +-- CreateTable +CREATE TABLE "public"."_ProductLikes" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL, + + CONSTRAINT "_ProductLikes_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateTable +CREATE TABLE "public"."_ArticleLikes" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL, + + CONSTRAINT "_ArticleLikes_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateTable +CREATE TABLE "public"."_CommentLikes" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL, + + CONSTRAINT "_CommentLikes_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateIndex +CREATE INDEX "_ProductLikes_B_index" ON "public"."_ProductLikes"("B"); + +-- CreateIndex +CREATE INDEX "_ArticleLikes_B_index" ON "public"."_ArticleLikes"("B"); + +-- CreateIndex +CREATE INDEX "_CommentLikes_B_index" ON "public"."_CommentLikes"("B"); + +-- AddForeignKey +ALTER TABLE "public"."_ProductLikes" ADD CONSTRAINT "_ProductLikes_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."_ProductLikes" ADD CONSTRAINT "_ProductLikes_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."_ArticleLikes" ADD CONSTRAINT "_ArticleLikes_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."_ArticleLikes" ADD CONSTRAINT "_ArticleLikes_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."_CommentLikes" ADD CONSTRAINT "_CommentLikes_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."_CommentLikes" ADD CONSTRAINT "_CommentLikes_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/part4-mission11/prisma/migrations/20250922033747_liketest/migration.sql b/part4-mission11/prisma/migrations/20250922033747_liketest/migration.sql new file mode 100644 index 000000000..45d5760b4 --- /dev/null +++ b/part4-mission11/prisma/migrations/20250922033747_liketest/migration.sql @@ -0,0 +1,103 @@ +/* + Warnings: + + - You are about to drop the column `likeCount` on the `Article` table. All the data in the column will be lost. + - You are about to drop the column `likeCount` on the `Comment` table. All the data in the column will be lost. + - You are about to drop the column `likeCount` on the `Product` table. All the data in the column will be lost. + - You are about to drop the `_ArticleLikes` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `_CommentLikes` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `_ProductLikes` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "public"."_ArticleLikes" DROP CONSTRAINT "_ArticleLikes_A_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."_ArticleLikes" DROP CONSTRAINT "_ArticleLikes_B_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."_CommentLikes" DROP CONSTRAINT "_CommentLikes_A_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."_CommentLikes" DROP CONSTRAINT "_CommentLikes_B_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."_ProductLikes" DROP CONSTRAINT "_ProductLikes_A_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."_ProductLikes" DROP CONSTRAINT "_ProductLikes_B_fkey"; + +-- AlterTable +ALTER TABLE "public"."Article" DROP COLUMN "likeCount"; + +-- AlterTable +ALTER TABLE "public"."Comment" DROP COLUMN "likeCount"; + +-- AlterTable +ALTER TABLE "public"."Product" DROP COLUMN "likeCount"; + +-- DropTable +DROP TABLE "public"."_ArticleLikes"; + +-- DropTable +DROP TABLE "public"."_CommentLikes"; + +-- DropTable +DROP TABLE "public"."_ProductLikes"; + +-- CreateTable +CREATE TABLE "public"."ProductLike" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "productId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ProductLike_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."ArticleLike" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "articleId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ArticleLike_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."CommentLike" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "commentId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "CommentLike_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "ProductLike_userId_productId_key" ON "public"."ProductLike"("userId", "productId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ArticleLike_userId_articleId_key" ON "public"."ArticleLike"("userId", "articleId"); + +-- CreateIndex +CREATE UNIQUE INDEX "CommentLike_userId_commentId_key" ON "public"."CommentLike"("userId", "commentId"); + +-- AddForeignKey +ALTER TABLE "public"."ProductLike" ADD CONSTRAINT "ProductLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."ProductLike" ADD CONSTRAINT "ProductLike_productId_fkey" FOREIGN KEY ("productId") REFERENCES "public"."Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."ArticleLike" ADD CONSTRAINT "ArticleLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."ArticleLike" ADD CONSTRAINT "ArticleLike_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "public"."Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."CommentLike" ADD CONSTRAINT "CommentLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."CommentLike" ADD CONSTRAINT "CommentLike_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "public"."Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/part4-mission11/prisma/migrations/20251103043014_notify/migration.sql b/part4-mission11/prisma/migrations/20251103043014_notify/migration.sql new file mode 100644 index 000000000..8a348e414 --- /dev/null +++ b/part4-mission11/prisma/migrations/20251103043014_notify/migration.sql @@ -0,0 +1,20 @@ +-- CreateEnum +CREATE TYPE "public"."NotificationType" AS ENUM ('PRICE_CHANGE', 'NEW_COMMENT'); + +-- CreateTable +CREATE TABLE "public"."Notification" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "type" "public"."NotificationType" NOT NULL, + "message" TEXT NOT NULL, + "productId" INTEGER, + "articleId" INTEGER, + "commentId" INTEGER, + "isRead" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Notification_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "public"."Notification" ADD CONSTRAINT "Notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/part4-mission11/prisma/migrations/migration_lock.toml b/part4-mission11/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..044d57cdb --- /dev/null +++ b/part4-mission11/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/part4-mission11/prisma/schema.prisma b/part4-mission11/prisma/schema.prisma new file mode 100644 index 000000000..70ae557c2 --- /dev/null +++ b/part4-mission11/prisma/schema.prisma @@ -0,0 +1,131 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? +// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id Int @id @default(autoincrement()) + username String @unique + email String @unique + images String[] + password String + products Product[] + articles Article[] + comments Comment[] + productLikes ProductLike[] + articleLikes ArticleLike[] + commentLikes CommentLike[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + Notification Notification[] +} + +model Product { + id Int @id @default(autoincrement()) + name String + description String + price Int + tags String[] + images String[] + comments Comment[] + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + likes ProductLike[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Article { + id Int @id @default(autoincrement()) + title String + content String + tags String[] + images String[] + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + comments Comment[] + likes ArticleLike[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Comment { + id Int @id @default(autoincrement()) + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + articleId Int? + article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade) + productId Int? + product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) + likes CommentLike[] +} + +model ProductLike { + id Int @id @default(autoincrement()) + userId Int + productId Int + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + + @@unique([userId, productId]) +} + +model ArticleLike { + id Int @id @default(autoincrement()) + userId Int + articleId Int + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) + + @@unique([userId, articleId]) +} + +model CommentLike { + id Int @id @default(autoincrement()) + userId Int + commentId Int + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade) + + @@unique([userId, commentId]) +} + +model Notification { + id Int @id @default(autoincrement()) + user User @relation(fields: [userId], references: [id]) + userId Int // 알림 받는 사람 + + type NotificationType + message String + + productId Int? // 상품 관련 알림일 수도 있고 + articleId Int? // 게시글 관련 알림일 수도 있음 + commentId Int? // 댓글 알림일 수도 있음 + + isRead Boolean @default(false) + createdAt DateTime @default(now()) +} + +enum NotificationType { + PRICE_CHANGE // 좋아요한 상품 가격 변동 + NEW_COMMENT // 내 게시글에 댓글 달림 +} diff --git a/part4-mission11/src/app.ts b/part4-mission11/src/app.ts new file mode 100644 index 000000000..ae3311b64 --- /dev/null +++ b/part4-mission11/src/app.ts @@ -0,0 +1,46 @@ +import cookieParser from 'cookie-parser'; +import cors from 'cors'; +import express from 'express'; +import path from 'path'; + +import { dirnameFromMeta } from './lib/dirname.js'; +import passport from './lib/passport/index.js'; +import errorHandler from './middlewares/errorHandler.js'; +import { requestLogger } from './middlewares/logger.js'; +import routes from './routes/index.js'; +import { setupSwagger } from './swagger.js'; + +export async function buildApp(_opts: { forTest?: boolean } = {}) { + const app = express(); + const __dirname_safe = dirnameFromMeta(import.meta.url); + const isProd = process.env.NODE_ENV === 'production'; + + // nginx + app.set('trust proxy', 1); + + app.use( + cors({ + origin: [process.env.CORS_ORIGIN || 'http://localhost:3001'], + credentials: true, + }) + ); + + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + + // dev/test 에서만 src/uploads 정적 서빙 + if (!isProd) { + app.use('/uploads', express.static(path.join(__dirname_safe, './uploads'))); + } + + app.use(cookieParser()); + app.use(passport.initialize()); + app.use(requestLogger); + + app.use('/', routes); + setupSwagger(app); + + app.use(errorHandler); + + return app; +} diff --git a/part4-mission11/src/config/multer.ts b/part4-mission11/src/config/multer.ts new file mode 100644 index 000000000..101cc2d4c --- /dev/null +++ b/part4-mission11/src/config/multer.ts @@ -0,0 +1,78 @@ +import { S3Client } from '@aws-sdk/client-s3'; +import multer, { type FileFilterCallback } from 'multer'; +import multerS3 from 'multer-s3'; +import path from 'path'; + +import { dirnameFromMeta } from '../lib/dirname.js'; + +const __dirname_safe = dirnameFromMeta(import.meta.url); + +const isProd = process.env.NODE_ENV === 'production'; + +/* ------------ 공통 파일 필터 (이미지 전용) ------------ */ +const fileFilter = ( + req: Express.Request, + file: Express.Multer.File, + cb: FileFilterCallback +) => { + const allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + if (allowedMimes.includes(file.mimetype)) { + cb(null, true); + } else { + const err = new multer.MulterError('LIMIT_UNEXPECTED_FILE'); + (err as unknown as { message: string }).message = + '이미지 파일만 업로드할 수 있습니다.'; + cb(err); + } +}; + +/* ------------ 스토리지 엔진 결정 (dev: 로컬, prod: S3) ------------ */ +let storage: multer.StorageEngine; + +if (isProd) { + // 프로덕션: S3로 업로드 + const s3 = new S3Client({ + region: process.env.AWS_REGION!, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID!, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, + }, + }); + + storage = multerS3({ + s3, + bucket: process.env.AWS_S3_BUCKET!, + acl: 'public-read', + metadata: (req, file, cb) => { + cb(null, { fieldName: file.fieldname }); + }, + key: (req, file, cb) => { + const ext = path.extname(file.originalname); + const filename = `${file.fieldname}-${Date.now()}${ext}`; + cb(null, filename); + }, + }); +} else { + // 개발/테스트: 로컬 디스크 저장 + storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, path.join(__dirname_safe, '../uploads')); + }, + filename: (req, file, cb) => { + const ext = path.extname(file.originalname); + cb(null, `${file.fieldname}-${Date.now()}${ext}`); + }, + }); +} + +/* ------------ Multer 미들웨어 ------------ */ +const upload = multer({ + storage, + fileFilter, + limits: { + fileSize: 5 * 1024 * 1024, // 5MB + files: 5, + }, +}); + +export default upload; diff --git a/part4-mission11/src/controllers/article-controller.ts b/part4-mission11/src/controllers/article-controller.ts new file mode 100644 index 000000000..e1a78ce9c --- /dev/null +++ b/part4-mission11/src/controllers/article-controller.ts @@ -0,0 +1,76 @@ +import type { Request, Response } from 'express'; + +import { articleService } from '../services/article-service.js'; + +class ArticleController { + // 게시글 + async getAllArticles(req: Request, res: Response) { + const userId = req.user?.id; + const { data, pagination } = await articleService.getAllArticles( + req.query, + userId + ); + res.json({ data, pagination }); + } + async getArticleById(req: Request, res: Response) { + const userId = req.user?.id; + const idParam = req.params.id; + const articleId = parseInt(idParam!, 10); + const article = await articleService.getArticleById(articleId, userId); + res.status(200).json({ data: article }); + } + + async createArticle(req: Request, res: Response) { + const { title, content } = req.body; + const userId = req.user!.id; + const newArticle = await articleService.createArticle( + title, + content, + userId + ); + res.status(201).json({ data: newArticle }); + } + + async updateArticle(req: Request, res: Response) { + const idParam = req.params.id; + const id = parseInt(idParam!, 10); + const { title, content, tags, images } = req.body; + const updateData = { title, content, tags, images }; + const userId = req.user!.id; + const updated = await articleService.updateArticle(id, userId, updateData); + res.json({ data: updated }); + } + + async deleteArticle(req: Request, res: Response) { + const idParam = req.params.id; + const id = parseInt(idParam!, 10); + const userId = req.user!.id; + await articleService.deleteArticle(id, userId); + res.status(204).send(); + } + + async likeArticle(req: Request, res: Response) { + const userId = req.user!.id; + const idParam = req.params.id; + const articleId = parseInt(idParam!, 10); + const article = await articleService.articleLike(userId, articleId); + res.json({ data: article }); + } + + async unlikeArticle(req: Request, res: Response) { + const userId = req.user!.id; + const idParam = req.params.id; + const articleId = parseInt(idParam!, 10); + const result = await articleService.articleUnlike(userId, articleId); + res.status(200).json({ data: result }); + } + + // 본인이 작성한 게시글 조회 + async getUserArticles(req: Request, res: Response) { + const userId = req.user!.id; + const articles = await articleService.getUserArticles(userId); + res.status(200).json({ data: articles }); + } +} + +export const articleController = new ArticleController(); diff --git a/part4-mission11/src/controllers/comments/article-comment-controller.ts b/part4-mission11/src/controllers/comments/article-comment-controller.ts new file mode 100644 index 000000000..a6dd1b201 --- /dev/null +++ b/part4-mission11/src/controllers/comments/article-comment-controller.ts @@ -0,0 +1,68 @@ +import type { Request, Response } from 'express'; + +import { articleCommentService } from '../../services/comments/article-comment-service.js'; + +class ArticleCommentController { + async getComments(req: Request, res: Response) { + const idParam = req.params.articleId; + const articleId = parseInt(idParam!, 10); + const userId = req.user?.id; + const comments = await articleCommentService.getCommentsByArticleId( + articleId, + userId + ); + res.json({ data: comments }); + } + + async createComment(req: Request, res: Response) { + const idParam = req.params.articleId; + const articleId = parseInt(idParam!, 10); + const { content } = req.body; + const userId = req.user!.id; + const newComment = await articleCommentService.createArticleComment( + articleId, + content, + userId + ); + return res.status(201).json({ data: newComment }); + } + + async updateComment(req: Request, res: Response) { + const idParam = req.params.commentId; + const commentId = parseInt(idParam!, 10); + const { content } = req.body; + const userId = req.user!.id; + const updated = await articleCommentService.updateComment( + commentId, + userId, + content + ); + res.json({ data: updated }); + } + + async deleteComment(req: Request, res: Response) { + const idParam = req.params.commentId; + const commentId = parseInt(idParam!, 10); + const userId = req.user!.id; + await articleCommentService.deleteComment(commentId, userId); + return res.status(204).send(); + } + + async likeComment(req: Request, res: Response) { + const idParam = req.params.commentId; + const commentId = parseInt(idParam!, 10); + const userId = req.user!.id; + const result = await articleCommentService.commentLike(userId, commentId); + res.status(200).json({ data: result }); + } + + async unlikeComment(req: Request, res: Response) { + const idParam = req.params.commentId; + const commentId = parseInt(idParam!, 10); + const userId = req.user!.id; + const result = await articleCommentService.commentUnlike(userId, commentId); + res.status(200).json({ data: result }); + } +} + +export const articleCommentController = new ArticleCommentController(); diff --git a/part4-mission11/src/controllers/comments/product-comment-controller.ts b/part4-mission11/src/controllers/comments/product-comment-controller.ts new file mode 100644 index 000000000..c1a11017f --- /dev/null +++ b/part4-mission11/src/controllers/comments/product-comment-controller.ts @@ -0,0 +1,68 @@ +import type { Request, Response } from 'express'; + +import { productCommentService } from '../../services/comments/product-comment-service.js'; + +class ProductCommentController { + async getComments(req: Request, res: Response) { + const idParam = req.params.productId; + const productId = parseInt(idParam!, 10); + const userId = req.user?.id; + const comments = await productCommentService.getCommentsByProductId( + productId, + userId + ); + res.json({ data: comments }); + } + + async createComment(req: Request, res: Response) { + const idParam = req.params.productId; + const productId = parseInt(idParam!, 10); + const { content } = req.body; + const userId = req.user!.id; + const newComment = await productCommentService.createProductComment( + productId, + content, + userId + ); + return res.status(201).json({ data: newComment }); + } + + async updateComment(req: Request, res: Response) { + const idParam = req.params.commentId; + const commentId = parseInt(idParam!, 10); + const { content } = req.body; + const userId = req.user!.id; + const updated = await productCommentService.updateComment( + commentId, + userId, + content + ); + res.json({ data: updated }); + } + + async deleteComment(req: Request, res: Response) { + const idParam = req.params.commentId; + const commentId = parseInt(idParam!, 10); + const userId = req.user!.id; + await productCommentService.deleteComment(commentId, userId); + return res.status(204).send(); + } + + async likeComment(req: Request, res: Response) { + const idParam = req.params.commentId; + const commentId = parseInt(idParam!, 10); + const userId = req.user!.id; + const result = await productCommentService.commentLike(userId, commentId); + res.status(200).json({ data: result }); + } + + async unlikeComment(req: Request, res: Response) { + const idParam = req.params.commentId; + const commentId = parseInt(idParam!, 10); + const userId = req.user!.id; + const result = await productCommentService.commentUnlike(userId, commentId); + res.status(200).json({ data: result }); + } +} + +export const productCommentController = new ProductCommentController(); diff --git a/part4-mission11/src/controllers/notification-controller.ts b/part4-mission11/src/controllers/notification-controller.ts new file mode 100644 index 000000000..a0b0d8af3 --- /dev/null +++ b/part4-mission11/src/controllers/notification-controller.ts @@ -0,0 +1,33 @@ +import type { Request, Response, NextFunction } from 'express'; + +import { notificationService } from '../services/notification-service.js'; +import type { AuthenticatedRequest } from '../types/authenticated-request.js'; + +class NotificationController { + async getMyNotifications(req: Request, res: Response, _next: NextFunction) { + const { user } = req as AuthenticatedRequest; + const list = await notificationService.getMyNotifications(user.id); + res.json(list); + } + + async getMyUnreadCount(req: Request, res: Response, _next: NextFunction) { + const { user } = req as AuthenticatedRequest; + const count = await notificationService.getMyUnreadCount(user.id); + res.json({ unreadCount: count }); + } + + async markAsRead(req: Request, res: Response, _next: NextFunction) { + const { user } = req as AuthenticatedRequest; + const { notificationId } = req.params; + await notificationService.markAsRead(user.id, Number(notificationId)); + res.status(204).end(); + } + + async markAllAsRead(req: Request, res: Response, _next: NextFunction) { + const { user } = req as AuthenticatedRequest; + await notificationService.markAllAsRead(user.id); + res.status(204).end(); + } +} + +export const notificationController = new NotificationController(); diff --git a/part4-mission11/src/controllers/product-controller.ts b/part4-mission11/src/controllers/product-controller.ts new file mode 100644 index 000000000..0d4e6d9d8 --- /dev/null +++ b/part4-mission11/src/controllers/product-controller.ts @@ -0,0 +1,98 @@ +import type { Request, Response } from 'express'; + +import { productService } from '../services/product-service.js'; + +class ProductController { + // 상품 + async getAllProducts(req: Request, res: Response) { + const userId = req.user?.id; + const { data, pagination } = await productService.getAllProducts( + req.query, + userId + ); + res.json({ data, pagination }); + } + + async getProductById(req: Request, res: Response) { + const idParam = req.params.id; + const id = parseInt(idParam!, 10); + const userId = req.user?.id; + const product = await productService.getProductById(id, userId); + res.status(200).json({ data: product }); + } + + async createProduct(req: Request, res: Response) { + const { name, description, price, tags } = req.body; + const newProduct = await productService.createProduct( + req.user!.id, + name, + description, + price, + tags + ); + res.status(201).json({ data: newProduct }); + } + + async updateProduct(req: Request, res: Response) { + const idParam = req.params.id; + const id = parseInt(idParam!, 10); + const { name, description, price, tags, images } = req.body; + const updateData = { name, description, price, tags, images }; + const updated = await productService.updateProduct( + id, + req.user!.id, + updateData + ); + res.json({ data: updated }); + } + + async updateProductPrice(req: Request, res: Response) { + const productId = parseInt(req.params.id!, 10); + const { newPrice } = req.body; + + const actorUserId = req.user!.id; + + const updated = await productService.updateProductPrice( + productId, + Number(newPrice), + actorUserId + ); + + res.json({ + message: '가격이 변경되었습니다.', + data: updated, + }); + } + + async deleteProduct(req: Request, res: Response) { + const idParam = req.params.id; + const id = parseInt(idParam!, 10); + await productService.deleteProduct(id, req.user!.id); + res.status(204).send(); + } + + async likeProduct(req: Request, res: Response) { + const userId = req.user!.id; + const idParam = req.params.id; + const productId = parseInt(idParam!, 10); + const product = await productService.productLike(userId, productId); + res.json({ data: product }); + } + + async unlikeProduct(req: Request, res: Response) { + const userId = req.user!.id; + const idParam = req.params.id; + const productId = parseInt(idParam!, 10); + const product = await productService.productUnlike(userId, productId); + res.json({ data: product }); + } + + // 본인이 등록한 상품 조회 + async getUserProducts(req: Request, res: Response) { + const userId = req.user!.id; + const products = await productService.getUserProducts(userId); + res.status(200).json({ data: products }); + } +} + +export const productController = new ProductController(); diff --git a/part4-mission11/src/controllers/user-controller.ts b/part4-mission11/src/controllers/user-controller.ts new file mode 100644 index 000000000..a616a54b8 --- /dev/null +++ b/part4-mission11/src/controllers/user-controller.ts @@ -0,0 +1,119 @@ +import type { Request, Response } from 'express'; + +import { articleService } from '../services/article-service.js'; +import { productService } from '../services/product-service.js'; +import { userService } from '../services/user-service.js'; + +class UserController { + // 회원가입 + async register(req: Request, res: Response) { + const { username, email, password } = req.body; + const user = await userService.register(username, email, password); + res.status(201).json({ + data: user, + message: '회원 가입 성공!', + }); + } + + // 로그인 + async login(req: Request, res: Response) { + const userId = req.user!.id; + const { accessToken, refreshToken } = await userService.login(userId); + userService.setTokenCookies(res, accessToken, refreshToken); + res.status(200).json({ + accessToken: accessToken, + refreshToken: refreshToken, + message: '로그인 되었습니다.', + }); + } + + // 로그아웃 + logout(req: Request, res: Response) { + userService.clearTokenCookies(res); + res.status(200).send({ message: '로그아웃 되었습니다.' }); + } + + // 유저 정보 조회 + async getUserProfile(req: Request, res: Response) { + const idParam = req.params.userId; + const userId = parseInt(idParam!, 10); + const profile = await userService.getUserProfile(userId); + if (!profile) { + return res.status(404).json({ message: '유저를 찾을 수 없습니다.' }); + } + res.status(200).json({ profile, message: '유저 프로필 조회!' }); + } + + // 유저 정보 수정 + async updateUserProfile(req: Request, res: Response) { + const { username, email, images } = req.body; // 수정할 필드만 뽑음 + const updateData = { username, email, images }; + const idParam = req.params.userId; + const userId = parseInt(idParam!, 10); + const updated = await userService.updateUserProfile(userId, updateData); + res.status(200).json({ updated, message: '프로필 수정 완료!' }); + } + + // 비밀번호 수정 + async updatePassword(req: Request, res: Response) { + const idParam = req.params.userId; + const userId = parseInt(idParam!, 10); + if (!Number.isFinite(userId)) { + return res.status(400).json({ message: '잘못된 사용자 ID 입니다.' }); + } + const { currentPassword, newPassword, newPasswordConfirm } = req.body; + if (newPassword !== newPasswordConfirm) { + return res + .status(400) + .json({ message: '새 비밀번호가 일치하지 않습니다.' }); + } + await userService.updatePassword(userId, currentPassword, newPassword); + res.status(200).json({ message: '비밀번호가 변경되었습니다.' }); + } + + // 유저가 단 댓글 조회 + async getUserComments(req: Request, res: Response) { + const idParam = req.params.userId; + const userId = parseInt(idParam!, 10); + if (!Number.isFinite(userId)) { + return res.status(400).json({ message: '잘못된 사용자 ID 입니다.' }); + } + const comments = await userService.getUserComments(userId); + res.status(200).json({ comments }); + } + + // 유저가 좋아요 누른 상품 조회 + async getUserLikedProducts(req: Request, res: Response) { + const idParam = req.params.userId; + const userId = parseInt(idParam!, 10); + if (!Number.isFinite(userId)) { + return res.status(400).json({ message: '잘못된 사용자 ID 입니다.' }); + } + const likedProducts = await productService.getUserLikedProducts(userId); + res.json({ data: likedProducts }); + } + + // 유저가 좋아요 누른 게시글 조회 + async getUserLikedArticles(req: Request, res: Response) { + const idParam = req.params.userId; + const userId = parseInt(idParam!, 10); + if (!Number.isFinite(userId)) { + return res.status(400).json({ message: '잘못된 사용자 ID 입니다.' }); + } + const likedArticles = await articleService.getUserLikedArticles(userId); + res.json({ data: likedArticles }); + } + + // 유저가 좋아요 누른 댓글 조회 + async getUserLikedComments(req: Request, res: Response) { + const idParam = req.params.userId; + const userId = parseInt(idParam!, 10); + if (!Number.isFinite(userId)) { + return res.status(400).json({ message: '잘못된 사용자 ID 입니다.' }); + } + const likedComments = await userService.getUserLikedComments(userId); + res.json({ data: likedComments }); + } +} + +export const userController = new UserController(); diff --git a/part4-mission11/src/dtos/article-dto.ts b/part4-mission11/src/dtos/article-dto.ts new file mode 100644 index 000000000..b33afad6f --- /dev/null +++ b/part4-mission11/src/dtos/article-dto.ts @@ -0,0 +1,61 @@ +import type { Prisma } from '@prisma/client'; + +import { prisma } from '../lib/prismaClient.js'; + +// DB에서 실제 Article 타입 +export type RawArticle = NonNullable< + Awaited> +>; + +// 응답용 DTO +export interface ArticleResponse { + id: number; + title: string; + content: string; + tags: string[]; + images: string[]; + likeCount: number; + isLiked: boolean; + user: { username: string }; + comments: CommentResponse[]; +} + +export interface CommentResponse { + id: number; + content: string; + likeCount: number; + isLiked: boolean; + user: { username: string }; +} + +export type ArticleWithRelations = Prisma.ArticleGetPayload<{ + include: { + likedBy: { select: { id: true } }; + comments: { + select: { + id: true; + content: true; + likeCount: true; + likedBy: { select: { id: true } }; + }; + }; + user: { select: { username: true } }; + }; +}>; + +export type ArticleQuery = { + page?: number; + limit?: number; + sort?: 'recent' | 'old'; + keyword?: string; + query?: string; + search?: string; +}; + +export interface UpdateArticleDto { + title?: string; + content?: string; + price?: number; + tags?: string[]; + images?: string[]; +} diff --git a/part4-mission11/src/dtos/product-dto.ts b/part4-mission11/src/dtos/product-dto.ts new file mode 100644 index 000000000..397bd8003 --- /dev/null +++ b/part4-mission11/src/dtos/product-dto.ts @@ -0,0 +1,90 @@ +// product-type.ts +import type { Prisma } from '@prisma/client'; + +import { prisma } from '../lib/prismaClient.js'; + +// DB에서 실제로 나오는 타입 +export type RawProduct = NonNullable< + Awaited> +>; + +// 응답용 DTO +export interface ProductResponse { + id: number; + name: string; + description: string; + price: number; + tags: string[]; + images: string[]; + userId: number; + likeCount: number; + isLiked: boolean; + comments: CommentResponse[]; +} + +// 댓글 응답 DTO +export interface CommentResponse { + id: number; + content: string; + likeCount: number; + isLiked: boolean; + user: { username: string }; +} + +export type ProductWithRelations = Prisma.ProductGetPayload<{ + include: { + likedBy: { select: { id: true } }; + comments: { + select: { + id: true; + content: true; + likeCount: true; + likedBy: { select: { id: true } }; + }; + }; + user: { select: { username: true } }; + }; +}>; + +export type ProductQuery = { + page?: number; + limit?: number; + sort?: 'recent' | 'old'; + keyword?: string; + query?: string; + search?: string; +}; + +export type ProductById = { + id: true; + name: true; + description: true; + price: true; + tags: true; + images: true; + userId: true; + likeCount: true; + likedBy: { select: { id: true; username: true } }; + comments: { + select: { + id: true; + content: true; + likeCount: true; + createdAt: true; + updatedAt: true; + user: { select: { username: true } }; + likedBy: { + where: { id: number }; + select: { id: true }; + }; + }; + }; +}; + +export interface UpdateProductDto { + name?: string; + description?: string; + price?: number; + tags?: string[]; + images?: string[]; +} diff --git a/part4-mission11/src/dtos/user-dto.ts b/part4-mission11/src/dtos/user-dto.ts new file mode 100644 index 000000000..a6b21f852 --- /dev/null +++ b/part4-mission11/src/dtos/user-dto.ts @@ -0,0 +1,41 @@ +import { prisma } from '../lib/prismaClient.js'; + +// prisma가 유저를 조회했을 때 나오는 실제 User 타입 +export type RawUser = NonNullable< + Awaited> +>; + +// Service 계층에서 DB에 업데이트할 때 사용하는 타입 +export type UserUpdateData = Partial>; + +export type UserPublic = Pick; + +// Prisma 유저 레코드에서 password 제거한 타입 +export type UserWithoutPassword = { + id: number; + username: string; + email: string; + images: string[] | null; +}; + +// Service 응답용 유저 프로필 DTO +export interface UserProfile { + id: number; + username: string; + email: string; + images: string[]; +} + +// 댓글 DTO +export interface CommentResponse { + content: string; + article?: { + id: number; + title: string; + } | null; + product?: { + id: number; + name: string; + } | null; + likeCount: number; +} diff --git a/part4-mission11/src/env.ts b/part4-mission11/src/env.ts new file mode 100644 index 000000000..6f6cc3536 --- /dev/null +++ b/part4-mission11/src/env.ts @@ -0,0 +1,7 @@ +import dotenv from 'dotenv'; + +if (process.env.NODE_ENV === 'test') { + dotenv.config({ quiet: true }); +} else { + dotenv.config(); +} diff --git a/part4-mission11/src/lib/appError.ts b/part4-mission11/src/lib/appError.ts new file mode 100644 index 000000000..7add7e608 --- /dev/null +++ b/part4-mission11/src/lib/appError.ts @@ -0,0 +1,10 @@ +export default class AppError extends Error { + public statusCode: number; + public isOperational: boolean; + + constructor(message: string, statusCode: number = 500) { + super(message); + this.statusCode = statusCode; + this.isOperational = true; + } +} diff --git a/part4-mission11/src/lib/constants.ts b/part4-mission11/src/lib/constants.ts new file mode 100644 index 000000000..5599b6315 --- /dev/null +++ b/part4-mission11/src/lib/constants.ts @@ -0,0 +1,30 @@ +declare global {} + +// NODE_ENV, PORT 등은 크게 문제 없음 +const NODE_ENV = process.env.NODE_ENV || 'development'; +const PORT = process.env.PORT || 3000; + +// 먼저 raw로 읽어온다 +const ACCESS_SECRET_RAW = process.env.ACCESS_TOKEN_SECRET; +const REFRESH_SECRET_RAW = process.env.REFRESH_TOKEN_SECRET; + +// 존재하지 않으면 서버 바로 중단 +if (!ACCESS_SECRET_RAW || !REFRESH_SECRET_RAW) { + throw new Error('JWT secrets are not set in environment variables'); +} + +// 여기서부터는 string으로 확정된 애만 export +const ACCESS_TOKEN_SECRET = ACCESS_SECRET_RAW as string; +const REFRESH_TOKEN_SECRET = REFRESH_SECRET_RAW as string; + +const ACCESS_TOKEN_COOKIE_NAME = 'access-token'; +const REFRESH_TOKEN_COOKIE_NAME = 'refresh-token'; + +export { + NODE_ENV, + PORT, + ACCESS_TOKEN_SECRET, + REFRESH_TOKEN_SECRET, + ACCESS_TOKEN_COOKIE_NAME, + REFRESH_TOKEN_COOKIE_NAME, +}; diff --git a/part4-mission11/src/lib/dirname.ts b/part4-mission11/src/lib/dirname.ts new file mode 100644 index 000000000..5b5bf449e --- /dev/null +++ b/part4-mission11/src/lib/dirname.ts @@ -0,0 +1,8 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; + +export function dirnameFromMeta(metaUrl: string) { + if (typeof __dirname !== 'undefined') return __dirname; // CJS + const __filename = fileURLToPath(metaUrl); // ESM + return path.dirname(__filename); +} diff --git a/part4-mission11/src/lib/exclude.ts b/part4-mission11/src/lib/exclude.ts new file mode 100644 index 000000000..4a7c743ba --- /dev/null +++ b/part4-mission11/src/lib/exclude.ts @@ -0,0 +1,9 @@ +export function exclude( + entity: T, + keys: Key[] +): Omit { + for (let key of keys) { + delete entity[key]; + } + return entity; +} diff --git a/part4-mission11/src/lib/passport/index.ts b/part4-mission11/src/lib/passport/index.ts new file mode 100644 index 000000000..77b35f182 --- /dev/null +++ b/part4-mission11/src/lib/passport/index.ts @@ -0,0 +1,20 @@ +import passport from 'passport'; + +import { accessTokenStrategy, refreshTokenStrategy } from './jwtStrategy.js'; +import { localStrategy } from './localStrategy.js'; + +// 전략 등록 +passport.use('local', localStrategy); +passport.use('access-token', accessTokenStrategy); +passport.use('refresh-token', refreshTokenStrategy); + +// 미들웨어 export +export const localAuth = passport.authenticate('local', { session: false }); +export const accessAuth = passport.authenticate('access-token', { + session: false, +}); +export const refreshAuth = passport.authenticate('refresh-token', { + session: false, +}); + +export default passport; diff --git a/part4-mission11/src/lib/passport/jwtStrategy.ts b/part4-mission11/src/lib/passport/jwtStrategy.ts new file mode 100644 index 000000000..93adef9db --- /dev/null +++ b/part4-mission11/src/lib/passport/jwtStrategy.ts @@ -0,0 +1,40 @@ +import { type JwtPayload } from 'jsonwebtoken'; +import { + Strategy as JwtStrategy, + ExtractJwt, + type VerifiedCallback, +} from 'passport-jwt'; + +import { ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET } from '../constants.js'; +import { prisma } from '../prismaClient.js'; + +const accessTokenOptions = { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: ACCESS_TOKEN_SECRET, +}; + +const refreshTokenOptions = { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: REFRESH_TOKEN_SECRET, +}; + +async function jwtVerify(payload: JwtPayload, done: VerifiedCallback) { + try { + const user = await prisma.user.findUnique({ + where: { id: payload.id }, + }); + done(null, user); + } catch (error) { + done(error, false); + } +} + +export const accessTokenStrategy = new JwtStrategy( + accessTokenOptions, + jwtVerify +); + +export const refreshTokenStrategy = new JwtStrategy( + refreshTokenOptions, + jwtVerify +); diff --git a/part4-mission11/src/lib/passport/localStrategy.ts b/part4-mission11/src/lib/passport/localStrategy.ts new file mode 100644 index 000000000..3d74ee4d5 --- /dev/null +++ b/part4-mission11/src/lib/passport/localStrategy.ts @@ -0,0 +1,51 @@ +import bcrypt from 'bcrypt'; +import * as PassportLocal from 'passport-local'; +const LocalStrategy = PassportLocal.Strategy; +import type { IVerifyOptions } from 'passport-local'; + +import type { AuthUser } from '../../types/authenticated-request.js'; +import { prisma } from '../prismaClient.js'; + +export const localStrategy = new LocalStrategy(async function ( + username: string, + password: string, + done: (error: unknown, user?: AuthUser | false, info?: IVerifyOptions) => void +) { + try { + // 1) 유저 조회 + const user = await prisma.user.findUnique({ + where: { username }, + select: { + id: true, + username: true, + email: true, + password: true, // 비교를 위해 필요하지만 최종 결과엔 안 넣음 + }, + }); + + if (!user) { + // 로그인 실패: 유저 없음 + return done(null, false); + } + + // 2) 비번 비교 + const isPasswordValid = await bcrypt.compare(password, user.password); + if (!isPasswordValid) { + // 로그인 실패: 비번 틀림 + return done(null, false); + } + + // 3) 민감정보 제거하고 AuthUser 형태로 변환 + const safeUser: AuthUser = { + id: user.id, + username: user.username, + email: user.email, + }; + + // 4) 로그인 성공 + return done(null, safeUser); + } catch (err) { + // DB 오류 등 + return done(err); + } +}); diff --git a/part4-mission11/src/lib/ports.ts b/part4-mission11/src/lib/ports.ts new file mode 100644 index 000000000..6e0fe3b48 --- /dev/null +++ b/part4-mission11/src/lib/ports.ts @@ -0,0 +1,23 @@ +import { wsGateway as realWs } from './ws.js'; + +export type WsPort = { + notifyUser: (args: { + userId: number; + type: 'NEW_COMMENT' | 'PRICE_CHANGE' | string; + message: string; + createdAt?: Date; + data?: Record; + }) => void; +}; + +const defaultPorts = { ws: realWs as WsPort }; + +export const ports: { ws: WsPort } = { ...defaultPorts }; + +export function setPorts(patch: Partial) { + Object.assign(ports, patch); +} + +export function resetPorts() { + ports.ws = defaultPorts.ws; +} diff --git a/part4-mission11/src/lib/prismaClient.ts b/part4-mission11/src/lib/prismaClient.ts new file mode 100644 index 000000000..23c750d83 --- /dev/null +++ b/part4-mission11/src/lib/prismaClient.ts @@ -0,0 +1,20 @@ +// src/lib/prismaClient.ts +import { PrismaClient } from '@prisma/client'; + +declare global { + // 글로벌 캐시용 + var __prisma: PrismaClient | undefined; +} + +/** + * PrismaClient 싱글톤 + * - 프로덕션: top-level export + * - 개발(TSX watch): 글로벌 캐시 사용 + */ +export const prisma: PrismaClient = + global.__prisma ?? + new PrismaClient(); + // {log: ['query', 'warn', 'error'],} + +// 개발 환경에서 글로벌 캐시에 저장 +if (process.env.NODE_ENV !== 'production') global.__prisma = prisma; diff --git a/part4-mission11/src/lib/token.ts b/part4-mission11/src/lib/token.ts new file mode 100644 index 000000000..0606fcf70 --- /dev/null +++ b/part4-mission11/src/lib/token.ts @@ -0,0 +1,57 @@ +import jwt from 'jsonwebtoken'; + +import AppError from './appError.js'; +import { ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET } from './constants.js'; + +type AccessPayload = jwt.JwtPayload & { sub: number; type: 'access' }; +type RefreshPayload = jwt.JwtPayload & { sub: number; type: 'refresh' }; + +export function generateTokens(userId: number) { + const accessToken = jwt.sign( + { sub: userId, type: 'access' } as AccessPayload, + ACCESS_TOKEN_SECRET, + { algorithm: 'HS256', expiresIn: '1h' } + ); + const refreshToken = jwt.sign( + { sub: userId, type: 'refresh' } as RefreshPayload, + REFRESH_TOKEN_SECRET, + { algorithm: 'HS256', expiresIn: '7d' } + ); + return { accessToken, refreshToken }; +} + +export function verifyAccessToken(token: string): { userId: number } { + try { + const d = jwt.verify(token, ACCESS_TOKEN_SECRET, { + algorithms: ['HS256'], + }) as AccessPayload; + if ( + typeof d !== 'object' || + d.type !== 'access' || + typeof d.sub !== 'number' + ) { + throw new AppError('유효하지 않은 액세스 토큰', 401); + } + return { userId: d.sub }; + } catch { + throw new AppError('유효하지 않은 액세스 토큰', 401); + } +} + +export function verifyRefreshToken(token: string): { userId: number } { + try { + const d = jwt.verify(token, REFRESH_TOKEN_SECRET, { + algorithms: ['HS256'], + }) as RefreshPayload; + if ( + typeof d !== 'object' || + d.type !== 'refresh' || + typeof d.sub !== 'number' + ) { + throw new AppError('유효하지 않은 리프레시 토큰', 401); + } + return { userId: d.sub }; + } catch { + throw new AppError('유효하지 않은 리프레시 토큰', 401); + } +} diff --git a/part4-mission11/src/lib/ws.ts b/part4-mission11/src/lib/ws.ts new file mode 100644 index 000000000..875be010d --- /dev/null +++ b/part4-mission11/src/lib/ws.ts @@ -0,0 +1,145 @@ +import type { NotificationType as PrismaNotificationType } from '@prisma/client'; +import { NotificationType as NotificationTypeValues } from '@prisma/client'; +import type { Server as HTTPServer } from 'http'; +import { Server } from 'socket.io'; + +import { parseUserIdFromToken } from './wsAuth.js'; +import type { NotificationType as DomainNotificationType } from '../types/notification.js'; + +type UserSocketMap = Map>; +const userSockets: UserSocketMap = new Map(); +let io: Server | undefined; + +type AuthFn = (rawToken: unknown) => number | null; + +export function __resetWsForTest() { + userSockets.clear(); + io = undefined; +} +export function __getUserSocketsForTest() { + return userSockets; +} + +type WireNotificationType = 'contract-linked' | 'chat' | 'system'; +export interface WireNotificationPayload { + type: WireNotificationType; + message: string; + createdAt: string; + data?: Record; +} + +export function mapDomainToWire( + t: PrismaNotificationType | DomainNotificationType +): WireNotificationType { + switch (t) { + case NotificationTypeValues.PRICE_CHANGE: + return 'system'; + case NotificationTypeValues.NEW_COMMENT: + return 'chat'; + default: { + const _never: never = t as never; + return 'system'; + } + } +} + +export function setupWebSocket(server: HTTPServer, deps?: { auth?: AuthFn }) { + const auth = deps?.auth ?? parseUserIdFromToken; + + io = new Server(server, { + path: '/ws', + cors: { + origin: [process.env.CORS_ORIGIN || 'http://localhost:3001'], + credentials: true, + methods: ['GET', 'POST'], + }, + }); + + io.on('connection', (socket) => { + const userId = auth(socket.handshake.auth?.token); + if (!userId) { + socket.disconnect(true); + return; + } + + const set = userSockets.get(userId) ?? new Set(); + set.add(socket.id); + userSockets.set(userId, set); + + socket.emit('joined', { ok: true, userId }); + + socket.on('disconnect', () => { + const s = userSockets.get(userId); + if (s) { + s.delete(socket.id); + if (s.size === 0) userSockets.delete(userId); + } + }); + }); +} + +// 알림 게이트웨이 +export const wsGateway = { + notifyUser(args: { + userId: number; + type: PrismaNotificationType | DomainNotificationType; + message: string; + createdAt?: Date; + data?: Record; + }) { + if (!io) return; + + const { userId, type, message, data } = args; + const createdAt = (args.createdAt ?? new Date()).toISOString(); + const wire: WireNotificationPayload = { + type: mapDomainToWire(type), + message, + createdAt, + ...(data !== undefined ? { data } : {}), + }; + + const sockets = userSockets.get(userId); + if (!sockets) return; + + for (const sid of sockets) { + io.to(sid).emit('notification', wire); + } + }, +}; + +// 전달받은 io를 실제로 사용 (전역 의존성 제거) +export function publishToUser( + sio: Server, + { + userId, + event, + payload, + }: { userId: number; event: 'notification'; payload: unknown } +) { + const sockets = userSockets.get(userId); + if (!sockets) return; + for (const sid of sockets) { + sio.to(sid).emit(event, payload); + } +} + +export async function closeWebSocket(): Promise { + if (!io) return; + + // 1) 모든 소켓 강제 disconnect + const sockets = await io.fetchSockets().catch(() => []); + for (const s of sockets) { + try { + s.disconnect(true); + } catch {} + } + + // 2) io 자체 종료를 "완료"까지 대기 + await new Promise((resolve) => { + io!.close(() => resolve()); + }); + + io = undefined; +} + +// export type { UserSocketMap }; // (원하면) diff --git a/part4-mission11/src/lib/wsAuth.ts b/part4-mission11/src/lib/wsAuth.ts new file mode 100644 index 000000000..8a68bc715 --- /dev/null +++ b/part4-mission11/src/lib/wsAuth.ts @@ -0,0 +1,15 @@ +import { verifyAccessToken as realVerifyAccessToken } from './token.js'; + +export function parseUserIdFromToken( + rawToken: unknown, + // 테스트에서 주입 가능 + verify: (t: string) => { userId: number } = realVerifyAccessToken +): number | null { + if (typeof rawToken !== 'string') return null; + try { + const { userId } = verify(rawToken); + return typeof userId === 'number' ? userId : null; + } catch { + return null; + } +} diff --git a/part4-mission11/src/middlewares/authorize.ts b/part4-mission11/src/middlewares/authorize.ts new file mode 100644 index 000000000..536b65f20 --- /dev/null +++ b/part4-mission11/src/middlewares/authorize.ts @@ -0,0 +1,103 @@ +import bcrypt from 'bcrypt'; +import type { Request, RequestHandler } from 'express'; + +import { prisma } from '../lib/prismaClient.js'; + +// params에 어떤 키들이 올 수 있는지 제네릭으로 지정. +type Params = { id?: string; commentId?: string; userId?: string }; + +// modelGetter는 req를 받아서 userId를 포함한 리소스를 반환(또는 null) +type ModelGetter = (req: Request) => Promise<{ userId: number } | null>; + +// 공통 로직 +export const isOwner = (modelGetter: ModelGetter): RequestHandler => { + return async (req, res, next) => { + const resource = await modelGetter(req as Request); + if (!resource) + return res.status(404).json({ message: '대상을 찾을 수 없습니다.' }); + const reqUserId = (req.user as { id?: number } | undefined)?.id; + if (typeof reqUserId !== 'number') { + return res.status(401).json({ message: '로그인이 필요합니다.' }); + } + if (resource.userId !== reqUserId) + return res.status(403).json({ message: '권한이 없습니다.' }); + next(); + }; +}; + +// 제품 권한 체크 +export const isProductOwner = isOwner((req) => + prisma.product.findUnique({ + where: { id: parseInt((req.params.id ?? '') as string, 10) || 0 }, + select: { userId: true }, + }) +); + +// 게시글 권한 체크 +export const isArticleOwner = isOwner((req) => + prisma.article.findUnique({ + where: { id: parseInt((req.params.id ?? '') as string, 10) || 0 }, + select: { userId: true }, + }) +); + +// 댓글 권한 체크 +export const isCommentOwner = isOwner((req) => + prisma.comment.findUnique({ + where: { id: parseInt((req.params.commentId ?? '') as string, 10) || 0 }, + select: { userId: true }, + }) +); + +// 본인인지 체크 +export const isUserSelf: RequestHandler = (req, res, next) => { + // :userId 또는 :id 둘 다 지원 + const targetIdRaw = (req.params.userId ?? req.params.id) as + | string + | undefined; + const targetId = Number.parseInt(targetIdRaw ?? '', 10); + + const reqUserId = (req.user as { id?: number } | undefined)?.id; + + if (typeof reqUserId !== 'number') { + return res.status(401).json({ message: '로그인이 필요합니다.' }); + } + + // 파라미터가 없거나 숫자가 아니면 권한 없음으로 처리(요구사항에 따라 400으로 바꿀 수도 있음) + if (!Number.isFinite(targetId) || targetId !== reqUserId) { + return res.status(403).json({ message: '권한이 없습니다.' }); + } + + next(); +}; + +// 비밀번호 체크 +export const verifyPassword: RequestHandler = async (req, res, next) => { + try { + const { currentPassword } = req.body as { currentPassword?: string }; + if (!currentPassword) { + return res + .status(400) + .json({ message: '현재 비밀번호를 입력해 주세요.' }); + } + + const reqUserId = (req.user as { id?: number } | undefined)?.id; + if (typeof reqUserId !== 'number') { + return res.status(401).json({ message: '로그인이 필요합니다.' }); + } + + const user = await prisma.user.findUnique({ where: { id: reqUserId } }); + if (!user) { + return res.status(404).json({ message: '사용자를 찾을 수 없습니다.' }); + } + + const isValid = await bcrypt.compare(currentPassword, user.password); + if (!isValid) { + return res.status(401).json({ message: '비밀번호가 일치하지 않습니다.' }); + } + + next(); + } catch (err) { + next(err); + } +}; diff --git a/part4-mission11/src/middlewares/errorHandler.ts b/part4-mission11/src/middlewares/errorHandler.ts new file mode 100644 index 000000000..0e024834d --- /dev/null +++ b/part4-mission11/src/middlewares/errorHandler.ts @@ -0,0 +1,205 @@ +import { Prisma } from '@prisma/client'; +import type { ErrorRequestHandler } from 'express'; +import multer from 'multer'; + +/** ---- Custom Errors ---- */ +export class AppError extends Error { + constructor(message: string, public statusCode = 500, name = 'AppError') { + super(message); + this.name = name; + } +} +export class BadRequestError extends AppError { + constructor(message = '잘못된 요청입니다.') { + super(message, 400, 'BadRequestError'); + } +} +export class NotFoundError extends AppError { + constructor(message = '리소스를 찾을 수 없습니다.') { + super(message, 404, 'NotFoundError'); + } +} +export class ForbiddenError extends AppError { + constructor(message = '권한이 없습니다.') { + super(message, 403, 'ForbiddenError'); + } +} +export class UnauthorizedError extends AppError { + constructor(message = '로그인이 필요합니다.') { + super(message, 401, 'UnauthorizedError'); + } +} + +/** ---- HttpError Guard ---- */ +interface HttpError extends Error { + statusCode?: number; +} +function isHttpError(err: unknown): err is HttpError { + return ( + !!err && typeof err === 'object' && 'message' in err && 'statusCode' in err + ); +} + +/** ---- Prisma mapping helpers ---- */ +type PrismaMapped = { + status: number; + error: string; + message: string; + code: string; +}; +type PrismaLike = { code?: string; meta?: { target?: unknown } }; + +/** 실제 Prisma KnownRequestError 전용(널 아님) */ +function mapPrismaKnown( + err: Prisma.PrismaClientKnownRequestError +): PrismaMapped { + const code = err.code; + const rawTarget = err.meta?.target; + const fields = + Array.isArray(rawTarget) && rawTarget.every((s) => typeof s === 'string') + ? (rawTarget as string[]) + : []; + + let status = 400; + let message = `DB 요청 에러 (${code})`; + const error = 'PrismaClientKnownRequestError'; + + if (code === 'P2002') { + if (fields.includes('username')) message = '이미 사용 중인 닉네임입니다.'; + else if (fields.includes('email')) message = '이미 사용 중인 이메일입니다.'; + else message = `중복된 값이 존재합니다. (${fields.join(', ')})`; + status = 409; + } else if (code === 'P2003') { + message = '잘못된 참조로 인해 작업이 불가능합니다.'; + status = 400; + } else if (code === 'P2025') { + message = '대상을 찾을 수 없습니다.'; + status = 404; + } + return { status, error, message, code }; +} + +/** 가짜/래핑된 Prisma-like 에러 폴백(없으면 null) */ +function mapPrismaLike(err: unknown): PrismaMapped | null { + const code = (err as PrismaLike)?.code; + if (typeof code !== 'string') return null; + + const rawTarget = (err as PrismaLike)?.meta?.target; + const fields = + Array.isArray(rawTarget) && rawTarget.every((s) => typeof s === 'string') + ? (rawTarget as string[]) + : []; + + let status = 400; + let message = `DB 요청 에러 (${code})`; + const error = 'PrismaClientKnownRequestError'; + + if (code === 'P2002') { + if (fields.includes('username')) message = '이미 사용 중인 닉네임입니다.'; + else if (fields.includes('email')) message = '이미 사용 중인 이메일입니다.'; + else message = `중복된 값이 존재합니다. (${fields.join(', ')})`; + status = 409; + } else if (code === 'P2003') { + message = '잘못된 참조로 인해 작업이 불가능합니다.'; + status = 400; + } else if (code === 'P2025') { + message = '대상을 찾을 수 없습니다.'; + status = 404; + } + return { status, error, message, code }; +} + +/** ---- Multer mapping ---- */ +function mapMulter(err: multer.MulterError) { + let message = err.message; + if (err.code === 'LIMIT_FILE_SIZE') + message = '파일 크기가 너무 큽니다. (최대 5MB)'; + else if (err.code === 'LIMIT_UNEXPECTED_FILE') + message = '허용되지 않는 파일 필드입니다.'; + else if (err.code === 'LIMIT_FILE_COUNT') + message = '최대 5개의 파일만 업로드할 수 있습니다.'; + return { status: 400, error: 'MulterError', message, code: err.code }; +} + +/** ---- Error Handler ---- */ +const errorHandler: ErrorRequestHandler = (err, req, res, _next) => { + console.error((err as Error)?.stack ?? err); + + const isProd = process.env.NODE_ENV === 'production'; + const isTest = process.env.NODE_ENV === 'test'; + + if (!isTest) { + console.error((err as Error)?.stack ?? err); + } + + // 기본값 + let status = 500; + let error = (err as Error)?.name || 'InternalServerError'; + let message = (err as Error)?.message || '서버 오류'; + let code: string | undefined; + + // 1) 커스텀 AppError + if (err instanceof AppError) { + status = err.statusCode; + error = err.name; + message = err.message; + } + // 2) HttpError + else if (isHttpError(err)) { + status = err.statusCode ?? 500; + error = (err as Error).name || error; + message = err.message ?? message; + } + // 3) Prisma KnownRequestError (확정 매핑: null 아님) + else if (err instanceof Prisma.PrismaClientKnownRequestError) { + const m = mapPrismaKnown(err); + status = m.status; + error = m.error; + message = m.message; + code = m.code; + } + // 4) MulterError + else if (err instanceof multer.MulterError) { + const m = mapMulter(err); + status = m.status; + error = m.error; + message = m.message; + code = m.code; + } + + // 5) 폴백: Prisma-like(code만 있는 가짜/래핑) 매핑 시도 + if (!code) { + const m2 = mapPrismaLike(err); + if (m2) { + status = m2.status; + error = m2.error; + message = m2.message; + code = m2.code; + } + } + + // 공통 응답 payload (단 한 번만 선언) + const payload: { + status: number; + error: string; + message: string; + code?: string; + stack?: string; + path?: string; + method?: string; + } = { status, error, message }; + + if (!isProd) { + if (code !== undefined) payload.code = code; + payload.path = req.originalUrl; + payload.method = req.method; + const st = (err as Error).stack; + if (st !== undefined) payload.stack = st; + } else { + if (code !== undefined) payload.code = code; + } + + return res.status(status).json(payload); +}; + +export default errorHandler; diff --git a/part4-mission11/src/middlewares/logger.ts b/part4-mission11/src/middlewares/logger.ts new file mode 100644 index 000000000..409bf60e5 --- /dev/null +++ b/part4-mission11/src/middlewares/logger.ts @@ -0,0 +1,15 @@ +import type { RequestHandler } from 'express'; + +export const requestLogger: RequestHandler = (req, res, next) => { + if (process.env.NODE_ENV === 'test') return next(); + const start = Date.now(); + + res.on('finish', () => { + const duration = Date.now() - start; + console.log( + `[백엔드] ${res.statusCode} ${req.method} ${req.originalUrl} - ${duration}ms` + ); + }); + + next(); +}; diff --git a/part4-mission11/src/middlewares/validation.ts b/part4-mission11/src/middlewares/validation.ts new file mode 100644 index 000000000..7a5824050 --- /dev/null +++ b/part4-mission11/src/middlewares/validation.ts @@ -0,0 +1,199 @@ +import type { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; + +import { prisma } from '../lib/prismaClient.js'; + +class Validation { + // ------------------------------ + // 상품 생성 + // ------------------------------ + productSchema = z.object({ + name: z.preprocess( + (val) => (val === undefined ? '' : val), + z.string().nonempty('상품 이름은 필수입니다.') + ), + description: z.preprocess( + (val) => (val === undefined ? '' : val), + z.string().nonempty('상품 설명은 필수입니다.') + ), + price: z.preprocess( + (val) => (val === undefined ? NaN : val), + z.number().positive('가격은 양수여야 합니다.') + ), + tags: z.array(z.string()).optional(), + }); + + // 상품 업데이트 + productUpdateSchema = z + .object({ + name: z.string().optional(), + description: z.string().optional(), + price: z.number().positive().optional(), + tags: z.array(z.string()).optional(), + }) + .refine((data) => Object.keys(data).length > 0, { + message: '수정할 데이터가 없습니다.', + }); + + productPriceUpdateSchema = z.object({ + newPrice: z.preprocess( + (val) => (val === undefined ? NaN : val), + z.number().positive('가격은 양수여야 합니다.') + ), + }); + + // ------------------------------ + // 게시글 생성 + // ------------------------------ + articleSchema = z.object({ + title: z.preprocess( + (val) => (val === undefined ? '' : val), + z.string().nonempty('제목은 필수입니다.') + ), + content: z.preprocess( + (val) => (val === undefined ? '' : val), + z.string().nonempty('내용은 필수입니다.') + ), + }); + + // 게시글 업데이트 + articleUpdateSchema = z + .object({ + title: z.string().optional(), + content: z.string().optional(), + }) + .refine((data) => Object.keys(data).length > 0, { + message: '수정할 데이터가 없습니다.', + }); + + // ------------------------------ + // 댓글 생성/업데이트 + // ------------------------------ + commentSchema = z.object({ + content: z.preprocess( + (val) => (val === undefined ? '' : val), + z.string().nonempty('내용은 필수입니다.') + ), + }); + + // 문자열 규칙만 담는 스키마 + passwordRules = z + .string() + .min(8, '비밀번호는 최소 8자 이상이어야 합니다.') + .max(64, '비밀번호는 최대 64자까지 가능합니다.') + .regex(/[A-Z]/, '대문자가 최소 1개 포함되어야 합니다.') + .regex(/[a-z]/, '소문자가 최소 1개 포함되어야 합니다.') + .regex(/[0-9]/, '숫자가 최소 1개 포함되어야 합니다.') + .regex(/[^A-Za-z0-9]/, '특수문자가 최소 1개 포함되어야 합니다.'); + + // 비번 변경용 스키마 (세 필드) + passwordSchema = z + .object({ + currentPassword: z.string().min(1, '현재 비밀번호는 필수입니다.'), + newPassword: this.passwordRules, // ← 이제 string + newPasswordConfirm: z.string().min(1, '새 비밀번호 확인은 필수입니다.'), + }) + .superRefine((val, ctx) => { + if (val.newPassword !== val.newPasswordConfirm) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['newPasswordConfirm'], + message: '새 비밀번호가 일치하지 않습니다.', + }); + } + }); + + // ------------------------------ + // ID 검증 + // ------------------------------ + idSchema = z.string().refine((val) => /^[1-9]\d*$/.test(val), { + message: 'ID가 올바르지 않습니다.', + }); + + // ------------------------------ + // 유저네임, 이메일 중복 검사 미들웨어 + // ------------------------------ + async validateRegister(req: Request, res: Response, next: NextFunction) { + const { username, email } = req.body; + + // username 중복 체크 + const usernameExists = await prisma.user.findUnique({ + where: { username }, + }); + if (usernameExists) { + return res.status(409).json({ message: '이미 사용 중인 닉네임입니다.' }); + } + + // email 중복 체크 + const emailExists = await prisma.user.findUnique({ where: { email } }); + if (emailExists) { + return res.status(409).json({ message: '이미 사용 중인 이메일입니다.' }); + } + + next(); + } + + // 목록 쿼리 스키마 (필요에 맞게 필드 추가/수정 OK) + listQuerySchema = z + .object({ + page: z.coerce.number().int().min(1).default(1), + pageSize: z.coerce.number().int().min(1).max(100).default(10), + sortBy: z.enum(['createdAt', 'updatedAt']).default('createdAt'), + order: z.enum(['asc', 'desc']).default('desc'), + searchBy: z.enum(['title', 'content', 'username']).optional(), + keyword: z.string().trim().optional(), + }) + .superRefine((val, ctx) => { + const usedSearch = + val.searchBy !== undefined || val.keyword !== undefined; + if (usedSearch && (!val.searchBy || !val.keyword)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'searchBy와 keyword를 함께 보내주세요.', + }); + } + }); + + // ------------------------------ + // 미들웨어용 스키마 검증 함수 + // ------------------------------ + validate(schema: z.ZodTypeAny) { + return (req: Request, res: Response, next: NextFunction) => { + const result = schema.safeParse(req.body); + if (!result.success) { + return res.status(400).json({ + message: result.error.issues.map((e) => e.message).join(', '), + }); + } + next(); + }; + } + + validateParam(paramName: string, schema: z.ZodTypeAny) { + return (req: Request, res: Response, next: NextFunction) => { + const result = schema.safeParse(req.params[paramName]); + if (!result.success) { + return res.status(400).json({ + message: result.error.issues.map((e) => e.message).join(', '), + }); + } + next(); + }; + } + + // 쿼리 검증 미들웨어 추가 + validateQuery(schema: z.ZodTypeAny) { + return (req: Request, res: Response, next: NextFunction) => { + const result = schema.safeParse(req.query); + if (!result.success) { + return res.status(400).json({ + message: result.error.issues.map((e) => e.message).join(', '), + }); + } + res.locals.query = result.data; + next(); + }; + } +} + +export const validation = new Validation(); diff --git a/part4-mission11/src/prisma/seed.ts b/part4-mission11/src/prisma/seed.ts new file mode 100644 index 000000000..43fdea670 --- /dev/null +++ b/part4-mission11/src/prisma/seed.ts @@ -0,0 +1,164 @@ +import bcrypt from 'bcrypt'; +import { prisma } from '../lib/prismaClient.js'; + +const main = async () => { + console.log('🔄 시드 시작...'); + + // 0️⃣ 모든 댓글/게시글/상품은 매번 삭제 후 재생성 + await prisma.comment.deleteMany(); + await prisma.article.deleteMany(); + await prisma.product.deleteMany(); + console.log('🧹 product / article / comment 초기화 완료'); + + // 1️⃣ 유저는 username unique 있으므로 upsert 가능 → 안전 + const hashedPassword1 = await bcrypt.hash('password1', 10); + const hashedPassword2 = await bcrypt.hash('password2', 10); + + const user1 = await prisma.user.upsert({ + where: { username: 'testUser1' }, + update: {}, + create: { + username: 'testUser1', + email: 'test1@example.com', + password: hashedPassword1, + images: [], + }, + }); + + const user2 = await prisma.user.upsert({ + where: { username: 'testUser2' }, + update: {}, + create: { + username: 'testUser2', + email: 'test2@example.com', + password: hashedPassword2, + images: [], + }, + }); + + console.log('👤 유저 upsert 완료'); + + // 2️⃣ 상품 재생성 (deleteMany 했으므로 create는 항상 성공) + const product1 = await prisma.product.create({ + data: { + name: 'Nintendo Switch2', + description: '아 스위치2 갖고 싶다', + price: 650000, + tags: ['전자제품'], + images: [], + userId: user1.id, + }, + }); + + const product2 = await prisma.product.create({ + data: { + name: 'PlayStation 5', + description: '게임 끝판왕', + price: 750000, + tags: ['게임기'], + images: [], + userId: user2.id, + }, + }); + + const product3 = await prisma.product.create({ + data: { + name: 'Xbox Series X', + description: 'MS 게임기', + price: 700000, + tags: ['게임기'], + images: [], + userId: user1.id, + }, + }); + + console.log('🛒 상품 생성 완료'); + + // 3️⃣ 게시글 재생성 + const article1 = await prisma.article.create({ + data: { + title: '스위치2 솔직히 너무 비싼듯 ㅇㅇ', + content: 'ㅈㄱㄴ', + tags: ['리뷰'], + userId: user2.id, + }, + }); + + const article2 = await prisma.article.create({ + data: { + title: '플스5 성능 리뷰', + content: '가격만 빼면 마음에 드네', + tags: ['리뷰', '게임'], + userId: user1.id, + }, + }); + + const article3 = await prisma.article.create({ + data: { + title: '엑스박스 시리즈 X 후기', + content: '엑스박스 쳤다...', + tags: ['리뷰', '게임'], + userId: user2.id, + }, + }); + + console.log('📝 게시글 생성 완료'); + + // 4️⃣ 댓글 재생성 + await prisma.comment.createMany({ + data: [ + // Product1 댓글 + { content: '와 가격', userId: user2.id, productId: product1.id }, + { + content: '스위치2 존버 대성공 ㅋㅋ', + userId: user1.id, + productId: product1.id, + }, + + // Product2 + { + content: '플스5 진짜 사고 싶다', + userId: user1.id, + productId: product2.id, + }, + + // Product3 + { content: '엑박도 좋음', userId: user2.id, productId: product3.id }, + + // Article1 + { + content: 'ㄹㅇ 쉽지않음 거의 플스5급 아님?', + userId: user1.id, + articleId: article1.id, + }, + { + content: '플스5 프로 생각하면 또 선녀 같네', + userId: user2.id, + articleId: article1.id, + }, + + // Article2 + { + content: '성능 리뷰 잘 봤습니다', + userId: user2.id, + articleId: article2.id, + }, + + // Article3 + { content: '엑박 후기 ㄳ', userId: user1.id, articleId: article3.id }, + ], + }); + + console.log('💬 댓글 생성 완료'); +}; + +main() + .then(() => { + console.log('🎉 데이터베이스 시딩 완료.'); + return prisma.$disconnect(); + }) + .catch(async (e) => { + console.error('❌ 시딩 에러:', e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/part4-mission11/src/repositories/article-repository.ts b/part4-mission11/src/repositories/article-repository.ts new file mode 100644 index 000000000..3afc63292 --- /dev/null +++ b/part4-mission11/src/repositories/article-repository.ts @@ -0,0 +1,91 @@ +import type { Prisma } from '@prisma/client'; + +import type { ArticleWithRelations } from '../dtos/article-dto.js'; +import { prisma } from '../lib/prismaClient.js'; + +class ArticleRepository { + async findMany( + args: Prisma.ArticleFindManyArgs + ): Promise { + return prisma.article.findMany(args) as Promise; + } + + async findUnique(articleId: number, userId?: number) { + return prisma.article.findUnique({ + where: { id: articleId }, + include: { + user: { select: { username: true } }, + comments: { + include: { + user: { select: { username: true } }, + }, + }, + ...(userId && { + likes: { where: { userId }, select: { userId: true } }, + }), + }, + }); + } + + async create(data: { title: string; content: string; userId: number }) { + return prisma.article.create({ + data: { + title: data.title, + content: data.content, + user: { connect: { id: data.userId } }, + }, + }); + } + + async update( + articleId: number, + userId: number, + updateData: Prisma.ArticleUpdateInput + ) { + return prisma.article.update({ + where: { id: articleId, userId }, + data: updateData, + }); + } + + async delete(articleId: number, userId: number) { + return prisma.article.deleteMany({ + where: { id: articleId, userId }, + }); + } + + async count(where?: Prisma.ArticleWhereInput) { + return prisma.article.count({ where: where ?? {} }); + } + + async findUserArticles(userId: number) { + return prisma.article.findMany({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + select: { + id: true, + title: true, + content: true, + tags: true, + images: true, + }, + }); + } + + async findLikedArticles(userId: number) { + return prisma.article.findMany({ + where: { + likes: { some: { userId } }, + }, + }); + } + + findLiteById(articleId: number) { + return prisma.article.findUnique({ + where: { id: articleId }, + select: { id: true, userId: true, title: true }, + }); + } +} + +export const articleRepository = new ArticleRepository(); diff --git a/part4-mission11/src/repositories/comments/article-comment-repository.ts b/part4-mission11/src/repositories/comments/article-comment-repository.ts new file mode 100644 index 000000000..1510102b8 --- /dev/null +++ b/part4-mission11/src/repositories/comments/article-comment-repository.ts @@ -0,0 +1,29 @@ +import { prisma } from '../../lib/prismaClient.js'; + +class ArticleCommentRepository { + findByArticleId(articleId: number) { + return prisma.comment.findMany({ + where: { articleId }, + select: { + id: true, + content: true, + createdAt: true, + updatedAt: true, + user: { select: { username: true } }, + }, + orderBy: { createdAt: 'desc' }, + }); + } + + create(articleId: number, content: string, userId: number) { + return prisma.comment.create({ + data: { + content, + userId, + articleId, + }, + }); + } +} + +export const articleCommentRepository = new ArticleCommentRepository(); diff --git a/part4-mission11/src/repositories/comments/comment-repository.ts b/part4-mission11/src/repositories/comments/comment-repository.ts new file mode 100644 index 000000000..b37660426 --- /dev/null +++ b/part4-mission11/src/repositories/comments/comment-repository.ts @@ -0,0 +1,27 @@ +import { prisma } from '../../lib/prismaClient.js'; + +class CommentRepository { + findById(commentId: number) { + return prisma.comment.findUnique({ + where: { id: commentId }, + include: { + user: { select: { username: true } }, + }, + }); + } + + update(commentId: number, content: string) { + return prisma.comment.update({ + where: { id: commentId }, + data: { content }, + }); + } + + delete(commentId: number, userId: number) { + return prisma.comment.deleteMany({ + where: { id: commentId, userId }, + }); + } +} + +export const commentRepository = new CommentRepository(); diff --git a/part4-mission11/src/repositories/comments/product-comment-repository.ts b/part4-mission11/src/repositories/comments/product-comment-repository.ts new file mode 100644 index 000000000..995a23c8c --- /dev/null +++ b/part4-mission11/src/repositories/comments/product-comment-repository.ts @@ -0,0 +1,44 @@ +import { prisma } from '../../lib/prismaClient.js'; + +class ProductCommentRepository { + findByProductId(productId: number) { + return prisma.comment.findMany({ + where: { productId }, + select: { + id: true, + content: true, + createdAt: true, + updatedAt: true, + user: { select: { username: true } }, + }, + orderBy: { createdAt: 'desc' }, + }); + } + + create(productId: number, content: string, userId: number) { + return prisma.comment.create({ + data: { + content, + userId, + productId, + }, + }); + } + + async countByCommentIds(commentIds: number[]) { + return prisma.commentLike.groupBy({ + by: ['commentId'], + _count: { commentId: true }, + where: { commentId: { in: commentIds } }, + }); + } + + async findByUserAndCommentIds(userId: number, commentIds: number[]) { + return prisma.commentLike.findMany({ + where: { userId, commentId: { in: commentIds } }, + select: { commentId: true }, + }); + } +} + +export const productCommentRepository = new ProductCommentRepository(); diff --git a/part4-mission11/src/repositories/like-repository.ts b/part4-mission11/src/repositories/like-repository.ts new file mode 100644 index 000000000..14355f7d2 --- /dev/null +++ b/part4-mission11/src/repositories/like-repository.ts @@ -0,0 +1,252 @@ +import type { Prisma } from '@prisma/client'; + +import { prisma } from '../lib/prismaClient.js'; + +/** + * 공통적으로 쓰는 유틸 타입 + */ +type ArticleLikeGroupByResult = { + articleId: number; + _count: { articleId: number }; +}; + +type ProductLikeGroupByResult = { + productId: number; + _count: { productId: number }; +}; + +type CommentLikeGroupByResult = { + commentId: number; + _count: { commentId: number }; +}; + +type ArticleUserLikeResult = { articleId: number }; +type ProductUserLikeResult = { productId: number }; +type CommentUserLikeResult = { commentId: number }; + +/** + * ArticleLike 전용 리포지토리 + */ +export class ArticleLikeRepository { + async create(userId: number, articleId: number) { + return prisma.articleLike.create({ + data: { userId, articleId }, + }); + } + + async delete(userId: number, articleId: number) { + return prisma.articleLike.delete({ + where: { + userId_articleId: { + userId, + articleId, + }, + }, + }); + } + + async count(articleId: number) { + return prisma.articleLike.count({ + where: { articleId }, + }); + } + + async exists(userId: number, articleId: number): Promise { + const record = await prisma.articleLike.findUnique({ + where: { + userId_articleId: { + userId, + articleId, + }, + }, + }); + return !!record; + } + + async countByTargetIds( + articleIds: number[] + ): Promise { + const rows = await prisma.articleLike.groupBy({ + by: ['articleId'], + _count: { articleId: true }, + where: { articleId: { in: articleIds } }, + }); + return rows; + } + + async findByUserAndTargetIds( + userId: number, + articleIds: number[] + ): Promise { + const rows = await prisma.articleLike.findMany({ + where: { + userId, + articleId: { in: articleIds }, + }, + select: { + articleId: true, + }, + }); + return rows; + } +} + +/** + * ProductLike 전용 리포지토리 + */ +export class ProductLikeRepository { + async create(userId: number, productId: number) { + return prisma.productLike.create({ + data: { userId, productId }, + }); + } + + async delete(userId: number, productId: number) { + return prisma.productLike.delete({ + where: { + userId_productId: { + userId, + productId, + }, + }, + }); + } + + async count(productId: number) { + return prisma.productLike.count({ + where: { productId }, + }); + } + + async exists(userId: number, productId: number): Promise { + const record = await prisma.productLike.findUnique({ + where: { + userId_productId: { + userId, + productId, + }, + }, + }); + return !!record; + } + + async countByTargetIds( + productIds: number[] + ): Promise { + const rows = await prisma.productLike.groupBy({ + by: ['productId'], + _count: { productId: true }, + where: { productId: { in: productIds } }, + }); + + return rows; + } + + async findByUserAndTargetIds( + userId: number, + productIds: number[] + ): Promise { + const rows = await prisma.productLike.findMany({ + where: { + userId, + productId: { in: productIds }, + }, + select: { + productId: true, + }, + }); + + return rows; + } + + /** + * 도메인 특화: 이 상품을 좋아요 누른 유저 목록 + */ + async findUserIdsWhoLikedProductTx( + tx: Prisma.TransactionClient, + productId: number + ) { + const likes = await tx.productLike.findMany({ + where: { productId }, + select: { userId: true }, + }); + + return likes.map((l) => l.userId); + } +} + +/** + * CommentLike 전용 리포지토리 + */ +export class CommentLikeRepository { + async create(userId: number, commentId: number) { + return prisma.commentLike.create({ + data: { userId, commentId }, + }); + } + + async delete(userId: number, commentId: number) { + return prisma.commentLike.delete({ + where: { + userId_commentId: { + userId, + commentId, + }, + }, + }); + } + + async count(commentId: number) { + return prisma.commentLike.count({ + where: { commentId }, + }); + } + + async exists(userId: number, commentId: number): Promise { + const record = await prisma.commentLike.findUnique({ + where: { + userId_commentId: { + userId, + commentId, + }, + }, + }); + return !!record; + } + + async countByTargetIds( + commentIds: number[] + ): Promise { + const rows = await prisma.commentLike.groupBy({ + by: ['commentId'], + _count: { commentId: true }, + where: { commentId: { in: commentIds } }, + }); + + return rows; + } + + async findByUserAndTargetIds( + userId: number, + commentIds: number[] + ): Promise { + const rows = await prisma.commentLike.findMany({ + where: { + userId, + commentId: { in: commentIds }, + }, + select: { + commentId: true, + }, + }); + + return rows; + } +} + +/** + * 실제 export해서 쓰는 인스턴스들 + */ +export const articleLikeRepository = new ArticleLikeRepository(); +export const productLikeRepository = new ProductLikeRepository(); +export const commentLikeRepository = new CommentLikeRepository(); diff --git a/part4-mission11/src/repositories/notification-repository.ts b/part4-mission11/src/repositories/notification-repository.ts new file mode 100644 index 000000000..7dce62edd --- /dev/null +++ b/part4-mission11/src/repositories/notification-repository.ts @@ -0,0 +1,54 @@ +import { prisma } from '../lib/prismaClient.js'; +import { AppError } from '../middlewares/errorHandler.js'; +import { type NotificationCreateInput } from '../types/notification.js'; + +class NotificationRepository { + create(data: NotificationCreateInput) { + return prisma.notification.create({ + data: { + ...data, + productId: data.productId ?? null, + articleId: data.articleId ?? null, + commentId: data.commentId ?? null, + }, + }); + } + + findByUserId(userId: number) { + return prisma.notification.findMany({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + }); + } + + countUnread(userId: number) { + return prisma.notification.count({ + where: { userId, isRead: false }, + }); + } + + async markAsRead(userId: number, notificationId: number) { + const result = await prisma.notification.updateMany({ + where: { + id: notificationId, + userId, + }, + data: { + isRead: true, + }, + }); + + if (result.count === 0) { + throw new AppError('알림을 찾을 수 없습니다.', 404); + } + } + + async markAllAsRead(userId: number) { + await prisma.notification.updateMany({ + where: { userId, isRead: false }, + data: { isRead: true }, + }); + } +} + +export const notificationRepository = new NotificationRepository(); diff --git a/part4-mission11/src/repositories/product-repository.ts b/part4-mission11/src/repositories/product-repository.ts new file mode 100644 index 000000000..289d0f841 --- /dev/null +++ b/part4-mission11/src/repositories/product-repository.ts @@ -0,0 +1,137 @@ +import type { Prisma } from '@prisma/client'; + +import type { ProductWithRelations } from '../dtos/product-dto.js'; +import { prisma } from '../lib/prismaClient.js'; + +class ProductRepository { + async findMany( + args: Prisma.ProductFindManyArgs + ): Promise { + return prisma.product.findMany(args) as Promise; + } + + async findUnique(productId: number, userId?: number) { + return prisma.product.findUnique({ + where: { id: productId }, + include: { + user: { select: { username: true } }, + comments: { + include: { + user: { select: { username: true } }, + }, + }, + ...(userId && { + likes: { where: { userId }, select: { userId: true } }, + }), + }, + }); + } + + findById(productId: number) { + return prisma.product.findUnique({ + where: { id: productId }, + }); + } + + async create(data: { + name: string; + description: string; + price: number; + tags: string[]; + userId: number; + }) { + return prisma.product.create({ + data: { + name: data.name, + description: data.description, + price: data.price, + tags: data.tags, + user: { connect: { id: data.userId } }, + }, + }); + } + + async updatePrice(productId: number, newPrice: number) { + return prisma.product.update({ + where: { id: productId }, + data: { price: newPrice }, + }); + } + + async update( + productId: number, + userId: number, + updateData: Prisma.ProductUpdateInput + ) { + return prisma.product.update({ + where: { id: productId, userId }, + data: updateData, + }); + } + + async delete(productId: number, userId: number) { + return prisma.product.deleteMany({ where: { id: productId, userId } }); + } + + async count(where?: Prisma.ProductWhereInput) { + return prisma.product.count({ where: where ?? {} }); + } + + async findUserProducts(userId: number) { + return prisma.product.findMany({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + select: { + id: true, + name: true, + description: true, + price: true, + tags: true, + images: true, + }, + }); + } + + async findLikedProducts(userId: number) { + return prisma.product.findMany({ + where: { + likes: { some: { userId } }, + }, + }); + } + + async countByProductIds(productIds: number[]) { + return prisma.productLike.groupBy({ + by: ['productId'], + _count: { productId: true }, + where: { productId: { in: productIds } }, + }); + } + + async findByUserAndProductIds(userId: number, productIds: number[]) { + return prisma.productLike.findMany({ + where: { userId, productId: { in: productIds } }, + select: { productId: true }, + }); + } + + findLiteById(productId: number) { + return prisma.product.findUnique({ + where: { id: productId }, + select: { id: true, userId: true, name: true }, + }); + } + + async updatePriceTx( + tx: Prisma.TransactionClient, + productId: number, + price: number + ) { + return tx.product.update({ + where: { id: productId }, + data: { price }, + }); + } +} + +export const productRepository = new ProductRepository(); diff --git a/part4-mission11/src/repositories/user-repository.ts b/part4-mission11/src/repositories/user-repository.ts new file mode 100644 index 000000000..001ff09b1 --- /dev/null +++ b/part4-mission11/src/repositories/user-repository.ts @@ -0,0 +1,99 @@ +import type { UserUpdateData } from '../dtos/user-dto.js'; +import { prisma } from '../lib/prismaClient.js'; + +class UserRepository { + async findByUsername(username: string) { + return prisma.user.findUnique({ where: { username } }); + } + + findUsernameById(userId: number) { + return prisma.user.findUnique({ + where: { id: userId }, + select: { username: true }, + }); + } + + async createUser(data: { + username: string; + email: string; + password: string; + }) { + return prisma.user.create({ data }); + } + + // 비밀번호 제외 버전 (프로필 조회 등) + async findById(userId: number) { + return prisma.user.findUnique({ + where: { id: userId }, + select: { + id: true, + username: true, + email: true, + images: true, + createdAt: true, + updatedAt: true, + }, + }); + } + + // 비밀번호 포함 버전 (인증/비밀번호 변경용) + async findByIdWithPassword(userId: number) { + return prisma.user.findUnique({ + where: { id: userId }, + }); + } + + async updateUser(userId: number, updateData: UserUpdateData) { + return prisma.user.update({ + where: { id: userId }, + data: updateData, + select: { + username: true, + email: true, + images: true, + }, + }); + } + + async updatePassword(userId: number, hashedPassword: string) { + return prisma.user.update({ + where: { id: userId }, + data: { password: hashedPassword }, + }); + } + + // 사용자가 작성한 댓글 조회 + async getUserComments(userId: number) { + return prisma.comment.findMany({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + select: { + id: true, + content: true, + article: { select: { id: true, title: true } }, + product: { select: { id: true, name: true } }, + createdAt: true, + }, + }); + } + + // 사용자가 좋아요한 댓글 조회 + async getUserLikedComments(userId: number) { + return prisma.commentLike.findMany({ + where: { userId }, + include: { + comment: { + select: { + id: true, + content: true, + article: { select: { id: true, title: true } }, + product: { select: { id: true, name: true } }, + createdAt: true, + }, + }, + }, + }); + } +} + +export const userRepository = new UserRepository(); diff --git a/part4-mission11/src/routes/article-router.ts b/part4-mission11/src/routes/article-router.ts new file mode 100644 index 000000000..fcf769faf --- /dev/null +++ b/part4-mission11/src/routes/article-router.ts @@ -0,0 +1,57 @@ +import express from 'express'; + +import { articleController } from '../controllers/article-controller.js'; +import { accessAuth } from '../lib/passport/index.js'; +import { isArticleOwner } from '../middlewares/authorize.js'; +import { validation } from '../middlewares/validation.js'; + +const router = express.Router(); + +// 게시글 조회, 생성 +router + .route('/') + .get( + validation.validateQuery(validation.listQuerySchema), + articleController.getAllArticles + ) + .post( + accessAuth, + validation.validate(validation.articleSchema), + articleController.createArticle + ); +// 게시글 상세 조회, 수정, 삭제 +router + .route('/:id') + .get( + validation.validateParam('id', validation.idSchema), + articleController.getArticleById + ) + .patch( + accessAuth, + validation.validateParam('id', validation.idSchema), + isArticleOwner, + validation.validate(validation.articleUpdateSchema), + articleController.updateArticle + ) + .delete( + accessAuth, + validation.validateParam('id', validation.idSchema), + isArticleOwner, + articleController.deleteArticle + ); + +// 게시글 좋아요, 좋아요 취소 +router + .route('/:id/like') + .post( + accessAuth, + validation.validateParam('id', validation.idSchema), + articleController.likeArticle + ) + .delete( + accessAuth, + validation.validateParam('id', validation.idSchema), + articleController.unlikeArticle + ); + +export default router; diff --git a/part4-mission11/src/routes/auth-router.ts b/part4-mission11/src/routes/auth-router.ts new file mode 100644 index 000000000..6abb669e9 --- /dev/null +++ b/part4-mission11/src/routes/auth-router.ts @@ -0,0 +1,25 @@ +import express from 'express'; + +import { REFRESH_TOKEN_COOKIE_NAME } from '../lib/constants.js'; +import { verifyRefreshToken, generateTokens } from '../lib/token.js'; +import { userService } from '../services/user-service.js'; + +const router = express.Router(); + +// 리프레시 토큰 +router.post('/refresh', async (req, res) => { + try { + const refreshToken = req.cookies[REFRESH_TOKEN_COOKIE_NAME]; + if (!refreshToken) return res.status(401).json({ message: '인증 오류' }); + + const { userId } = verifyRefreshToken(refreshToken); + const { accessToken, refreshToken: newRefreshToken } = + generateTokens(userId); + userService.setTokenCookies(res, accessToken, newRefreshToken); + return res.json({ accessToken }); + } catch (_err) { + return res.status(401).json({ message: '인증 오류' }); + } +}); + +export default router; diff --git a/part4-mission11/src/routes/comments/article-comment-router.ts b/part4-mission11/src/routes/comments/article-comment-router.ts new file mode 100644 index 000000000..20a89d02c --- /dev/null +++ b/part4-mission11/src/routes/comments/article-comment-router.ts @@ -0,0 +1,51 @@ +import express from 'express'; + +import { articleCommentController } from '../../controllers/comments/article-comment-controller.js'; +import { accessAuth } from '../../lib/passport/index.js'; +import { isCommentOwner } from '../../middlewares/authorize.js'; +import { validation } from '../../middlewares/validation.js'; + +const router = express.Router(); + +router + .route('/:articleId/comments') + .get(articleCommentController.getComments) + .post( + accessAuth, + validation.validateParam('articleId', validation.idSchema), + validation.validate(validation.commentSchema), + articleCommentController.createComment + ); + +// 댓글 수정, 삭제 +router + .route('/comments/:commentId') + .patch( + accessAuth, + isCommentOwner, + validation.validateParam('commentId', validation.idSchema), + validation.validate(validation.commentSchema), + articleCommentController.updateComment + ) + .delete( + accessAuth, + isCommentOwner, + validation.validateParam('commentId', validation.idSchema), + articleCommentController.deleteComment + ); + +// 게시글 댓글 좋아요 +router + .route('/comments/:commentId/like') + .post( + accessAuth, + validation.validateParam('commentId', validation.idSchema), + articleCommentController.likeComment + ) + .delete( + accessAuth, + validation.validateParam('commentId', validation.idSchema), + articleCommentController.unlikeComment + ); + +export default router; diff --git a/part4-mission11/src/routes/comments/product-comment-router.ts b/part4-mission11/src/routes/comments/product-comment-router.ts new file mode 100644 index 000000000..83c467e35 --- /dev/null +++ b/part4-mission11/src/routes/comments/product-comment-router.ts @@ -0,0 +1,51 @@ +import express from 'express'; + +import { productCommentController } from '../../controllers/comments/product-comment-controller.js'; +import { accessAuth } from '../../lib/passport/index.js'; +import { isCommentOwner } from '../../middlewares/authorize.js'; +import { validation } from '../../middlewares/validation.js'; + +const router = express.Router(); + +// 상품 댓글 조회, 작성 +router + .route('/:productId/comments') + .get(productCommentController.getComments) + .post( + accessAuth, + validation.validate(validation.commentSchema), + productCommentController.createComment + ); + +// 상품 댓글 수정, 삭제 +router + .route('/comments/:commentId') + .patch( + accessAuth, + validation.validateParam('commentId', validation.idSchema), + isCommentOwner, + validation.validate(validation.commentSchema), + productCommentController.updateComment + ) + .delete( + accessAuth, + validation.validateParam('commentId', validation.idSchema), + isCommentOwner, + productCommentController.deleteComment + ); + +// 상품 댓글 좋아요, 좋아요 취소 +router + .route('/comments/:commentId/like') + .post( + accessAuth, + validation.validateParam('commentId', validation.idSchema), + productCommentController.likeComment + ) + .delete( + accessAuth, + validation.validateParam('commentId', validation.idSchema), + productCommentController.unlikeComment + ); + +export default router; diff --git a/part4-mission11/src/routes/image-router.ts b/part4-mission11/src/routes/image-router.ts new file mode 100644 index 000000000..b12ab0aa9 --- /dev/null +++ b/part4-mission11/src/routes/image-router.ts @@ -0,0 +1,50 @@ +import express, { type Request, type Response } from 'express'; + +import upload from '../config/multer.js'; + +const router = express.Router(); +const isProd = process.env.NODE_ENV === 'production'; + +router.post('/upload/single', upload.single('myImage'), (req, res) => { + if (!req.file) { + return res.status(400).send('파일이 업로드되지 않았습니다.'); + } + + const file = req.file; + const filename = file.key ?? file.filename; + const url = + isProd && file.location ? file.location : `/uploads/${file.filename}`; + + res.status(200).json({ + message: '파일이 성공적으로 업로드되었습니다.', + filename, + url, + }); +}); + +router.post( + '/upload/array', + upload.array('myImages', 5), + (req: Request, res: Response) => { + const files = req.files as Express.Multer.File[] | undefined; + + if (!files || files.length === 0) { + return res.status(400).send('파일이 업로드되지 않았습니다.'); + } + + const result = files.map((file) => { + const filename = file.key ?? file.filename; + const url = + isProd && file.location ? file.location : `/uploads/${file.filename}`; + + return { filename, url }; + }); + + res.status(200).json({ + message: '파일들이 성공적으로 업로드되었습니다.', + files: result, + }); + } +); + +export default router; diff --git a/part4-mission11/src/routes/index.ts b/part4-mission11/src/routes/index.ts new file mode 100644 index 000000000..548174357 --- /dev/null +++ b/part4-mission11/src/routes/index.ts @@ -0,0 +1,23 @@ +import express from 'express'; + +import articleRouter from './article-router.js'; +import authRouter from './auth-router.js'; +import articleCommentRouter from './comments/article-comment-router.js'; +import productCommentRouter from './comments/product-comment-router.js'; +import imageRouter from './image-router.js'; +import notificationRouter from './notification-router.js'; +import productRouter from './product-router.js'; +import userRouter from './user-router.js'; + +const router = express.Router(); + +router.use('/users', userRouter); +router.use('/products', productRouter); +router.use('/products', productCommentRouter); +router.use('/articles', articleRouter); +router.use('/articles', articleCommentRouter); +router.use('/images', imageRouter); +router.use('/auth', authRouter); +router.use('/notifications', notificationRouter); + +export default router; diff --git a/part4-mission11/src/routes/notification-router.ts b/part4-mission11/src/routes/notification-router.ts new file mode 100644 index 000000000..4b2110bc8 --- /dev/null +++ b/part4-mission11/src/routes/notification-router.ts @@ -0,0 +1,23 @@ +import { Router } from 'express'; + +import { notificationController } from '../controllers/notification-controller.js'; +import { accessAuth } from '../lib/passport/index.js'; +import { validation } from '../middlewares/validation.js'; + +const router = Router(); + +router.get('/', accessAuth, notificationController.getMyNotifications); +router.get( + '/unread-count', + accessAuth, + notificationController.getMyUnreadCount +); +router.patch( + '/:notificationId/read', + accessAuth, + validation.validateParam('notificationId', validation.idSchema), + notificationController.markAsRead +); +router.patch('/read-all', accessAuth, notificationController.markAllAsRead); + +export default router; diff --git a/part4-mission11/src/routes/product-router.ts b/part4-mission11/src/routes/product-router.ts new file mode 100644 index 000000000..0e3973b78 --- /dev/null +++ b/part4-mission11/src/routes/product-router.ts @@ -0,0 +1,64 @@ +import express from 'express'; + +import { productController } from '../controllers/product-controller.js'; +import { accessAuth } from '../lib/passport/index.js'; +import { isProductOwner } from '../middlewares/authorize.js'; +import { validation } from '../middlewares/validation.js'; + +const router = express.Router(); + +// 상품 조회, 등록 +router + .route('/') + .get(productController.getAllProducts) + .post( + accessAuth, + validation.validate(validation.productSchema), + productController.createProduct + ); + +// 상품 상세 조회, 수정, 삭제 +router + .route('/:id') + .get( + validation.validateParam('id', validation.idSchema), + productController.getProductById + ) + .patch( + accessAuth, + validation.validateParam('id', validation.idSchema), + isProductOwner, + validation.validate(validation.productUpdateSchema), + productController.updateProduct + ) + .delete( + accessAuth, + validation.validateParam('id', validation.idSchema), + isProductOwner, + productController.deleteProduct + ); + +// 상품 좋아요, 좋아요 취소 +router + .route('/:id/like') + .post( + accessAuth, + validation.validateParam('id', validation.idSchema), + productController.likeProduct + ) + .delete( + accessAuth, + validation.validateParam('id', validation.idSchema), + productController.unlikeProduct + ); + +router.patch( + '/:id/price', + accessAuth, + validation.validateParam('id', validation.idSchema), + isProductOwner, + validation.validate(validation.productPriceUpdateSchema), + productController.updateProductPrice +); + +export default router; diff --git a/part4-mission11/src/routes/user-router.ts b/part4-mission11/src/routes/user-router.ts new file mode 100644 index 000000000..89f05ec83 --- /dev/null +++ b/part4-mission11/src/routes/user-router.ts @@ -0,0 +1,516 @@ +import express from 'express'; + +import { articleController } from '../controllers/article-controller.js'; +import { productController } from '../controllers/product-controller.js'; +import { userController } from '../controllers/user-controller.js'; +import { localAuth, accessAuth } from '../lib/passport/index.js'; +import { isUserSelf } from '../middlewares/authorize.js'; +import { validation } from '../middlewares/validation.js'; + +const router = express.Router(); + +// 등록 +/** + * @openapi + * /users/register: + * post: + * summary: 회원가입 + * tags: + * - User + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - username + * - email + * - password + * properties: + * username: + * type: string + * example: "shim" + * email: + * type: string + * format: email + * example: "shim@example.com" + * password: + * type: string + * format: password + * example: "mypassword123" + * responses: + * 201: + * description: 회원가입 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * username: + * type: string + * example: "shim" + * email: + * type: string + * example: "shim@example.com" + * message: + * type: string + * example: "회원 가입 성공!" + * 409: + * description: 닉네임 중복입니다. + */ +router.post('/register', validation.validateRegister, userController.register); + +// 로그인&로그아웃 +/** + * @openapi + * /users/login: + * post: + * summary: 로그인 + * tags: + * - User + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - username + * - password + * properties: + * username: + * type: string + * example: "johndoe" + * password: + * type: string + * format: password + * example: "mypassword123" + * responses: + * 200: + * description: 로그인 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * type: object + * properties: + * accessToken : + * type: string + * example: "abcdefg" + * refreshToken: + * type: string + * example: "abcdefg" + * message: + * type: string + * example: "로그인 성공!" + * 403: + * description: '아이디 혹은 패스워드가 일치하지 않습니다' + */ +router.post('/login', localAuth, userController.login); + +// 로그인&로그아웃 +/** + * @openapi + * /users/logout: + * post: + * summary: 로그아웃 + * tags: + * - User + * requestBody: + * required: false + * responses: + * 200: + * description: 로그아웃 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "로그아웃 되었습니다." + * 401: + * description: 인증되지 않은 사용자 + */ +router.post('/logout', userController.logout); + +// 유저 조회, 정보 수정 +/** + * @openapi + * /users/{userId}: + * get: + * summary: 유저 프로필 조회 + * tags: + * - User + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: integer + * description: 조회할 유저 ID + * responses: + * 200: + * description: 프로필 조회 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: integer + * username: + * type: string + * email: + * type: string + * images: + * type: array + * items: + * type: string + * 401: + * description: 인증되지 않은 사용자 + * 403: + * description: 자기 자신의 프로필만 조회 가능 + */ + +/** + * @openapi + * /users/{userId}: + * patch: + * summary: 유저 프로필 수정 + * tags: + * - User + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: integer + * description: 수정할 유저 ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * username: + * type: string + * example: "newname" + * email: + * type: string + * example: "newemail@example.com" + * images: + * type: array + * items: + * type: string + * example: ["image1.jpg", "image2.png"] + * responses: + * 200: + * description: 프로필 수정 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * profile: + * type: object + * properties: + * id: + * type: integer + * username: + * type: string + * email: + * type: string + * images: + * type: array + * items: + * type: string + * example: ["image1.jpg", "image2.png"] + * 401: + * description: 인증되지 않은 사용자 + * 403: + * description: 자기 자신의 프로필만 수정 가능 + */ +router + .route('/:userId') + .get( + accessAuth, + isUserSelf, + validation.validateParam('userId', validation.idSchema), + userController.getUserProfile + ) + .patch( + accessAuth, + isUserSelf, + validation.validateParam('userId', validation.idSchema), + userController.updateUserProfile + ); + +// 유저 비밀번호 수정 +/** + * @openapi + * /users/{userId}/password: + * patch: + * summary: 비밀번호 변경 + * tags: + * - User + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: integer + * description: 유저 비밀번호 변경 + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: true + * properties: + * currentPassword: + * type: string + * example: "password" + * newPassword: + * type: string + * example: "newpassword" + * newPasswordConfirm: + * type: string + * example: "newpassword" + * responses: + * 200: + * description: 비밀번호 변경 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "비밀번호가 변경되었습니다." + * 400: + * description: "새 비밀번호가 일치하지 않습니다." + * 404: + * description: "사용자를 찾을 수 없습니다." + */ +router.patch( + '/:userId/password', + accessAuth, + isUserSelf, + validation.validate(validation.passwordSchema), + userController.updatePassword +); + +// 자신의 상품, 게시글, 댓글 조회 +/** + * @openapi + * /users/{userId}/my-products: + * get: + * summary: 자신이 등록한 상품 조회 + * tags: + * - User + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: integer + * description: 조회할 유저 id + * responses: + * 200: + * description: 자신이 등록한 상품 조회 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: integer + * example: "1" + * name: + * type: string + * example: "name" + * description: + * type: string + * example: "description" + * price: + * type: integer + * example: "10000" + * tags: + * type: array + * items: + * type: string + * example: ["tag1", "tag2"] + * images: + * type: array + * items: + * type: string + * example: ["image1.jpg", "image2.png"] + * likeCount: + * type: integer + * example: "1" + * 403: + * description: "권한이 없습니다." + */ +/** + * @openapi + * /users/{userId}/my-articles: + * get: + * summary: 자신이 등록한 게시글 조회 + * tags: + * - User + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: integer + * description: 조회할 유저 id + * responses: + * 200: + * description: 자신이 등록한 게시글 조회 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: integer + * example: "1" + * title: + * type: string + * example: "title" + * content: + * type: string + * example: "content" + * price: + * type: integer + * example: "10000" + * tags: + * type: array + * items: + * type: string + * example: ["tag1", "tag2"] + * images: + * type: array + * items: + * type: string + * example: ["image1.jpg", "image2.png"] + * likeCount: + * type: integer + * example: "1" + * 403: + * description: "권한이 없습니다." + */ +/** + * @openapi + * /users/{userId}/my-comments: + * get: + * summary: 자신이 등록한 댓글 조회 + * tags: + * - User + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: integer + * description: 조회할 유저 ID + * responses: + * 200: + * description: 자신이 등록한 댓글 조회 성공 + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * content: + * type: string + * example: "댓글 내용입니다." + * article: + * type: object + * nullable: true + * properties: + * id: + * type: integer + * example: 1 + * title: + * type: string + * example: "게시글 제목" + * product: + * type: object + * nullable: true + * properties: + * id: + * type: integer + * example: 2 + * name: + * type: string + * example: "상품명" + * likeCount: + * type: integer + * example: 5 + * 403: + * description: "권한이 없습니다." + */ +router.get( + '/:userId/my-products', + accessAuth, + isUserSelf, + validation.validateParam('userId', validation.idSchema), + productController.getUserProducts +); +router.get( + '/:userId/my-articles', + accessAuth, + isUserSelf, + validation.validateParam('userId', validation.idSchema), + articleController.getUserArticles +); +router.get( + '/:userId/my-comments', + accessAuth, + isUserSelf, + validation.validateParam('userId', validation.idSchema), + userController.getUserComments +); + +// 좋아요한 상품, 게시글, 댓글 조회 +router.get( + '/:userId/likes/products', + accessAuth, + isUserSelf, + validation.validateParam('userId', validation.idSchema), + userController.getUserLikedProducts +); + +router.get( + '/:userId/likes/articles', + accessAuth, + isUserSelf, + validation.validateParam('userId', validation.idSchema), + userController.getUserLikedArticles +); + +router.get( + '/:userId/likes/comments', + accessAuth, + isUserSelf, + validation.validateParam('userId', validation.idSchema), + userController.getUserLikedComments +); + +export default router; diff --git a/part4-mission11/src/server.ts b/part4-mission11/src/server.ts new file mode 100644 index 000000000..aa03608b6 --- /dev/null +++ b/part4-mission11/src/server.ts @@ -0,0 +1,25 @@ +import './env.js'; +import http from 'http'; + +import { buildApp } from './app.js'; +import { setupWebSocket } from './lib/ws.js'; + +async function startServer() { + try { + const app = await buildApp({ forTest: false }); + + const server = http.createServer(app); + setupWebSocket(server); + + const PORT = Number(process.env.PORT) || 3000; + server.listen(PORT, '0.0.0.0', () => { + // eslint-disable-next-line no-console + console.log(`Server is running on http://localhost:${PORT}`); + }); + } catch (e) { + console.error(e); + process.exit(1); + } +} + +startServer(); diff --git a/part4-mission11/src/services/article-service.ts b/part4-mission11/src/services/article-service.ts new file mode 100644 index 000000000..ab7d27264 --- /dev/null +++ b/part4-mission11/src/services/article-service.ts @@ -0,0 +1,213 @@ +import { Prisma } from '@prisma/client'; + +import type { ArticleQuery, UpdateArticleDto } from '../dtos/article-dto.js'; +import AppError from '../lib/appError.js'; +import { articleRepository } from '../repositories/article-repository.js'; +import { + articleLikeRepository, + commentLikeRepository, +} from '../repositories/like-repository.js'; + +class ArticleService { + // 전체 게시글 조회 + async getAllArticles(query: ArticleQuery, userId?: number) { + const page = Number(query.page) || 1; + const limit = Number(query.limit) || 10; + const skip = (page - 1) * limit; + const sort = query.sort === 'old' ? 'old' : 'recent'; + const search = (query.keyword ?? query.query ?? query.search ?? '').trim(); + + const orderBy: Prisma.ArticleOrderByWithRelationInput = + sort === 'old' ? { createdAt: 'asc' } : { createdAt: 'desc' }; + + const where: Prisma.ArticleWhereInput = search + ? { + OR: [ + { title: { contains: search, mode: 'insensitive' } }, + { content: { contains: search, mode: 'insensitive' } }, + ], + } + : {}; + + const articles = await articleRepository.findMany({ + skip, + take: limit, + where, + orderBy, + include: { + user: { select: { username: true } }, + comments: { + include: { user: { select: { username: true } } }, + }, + }, + }); + + const articleIds = articles.map((a) => a.id); + const commentIds = articles.flatMap((a) => a.comments.map((c) => c.id)); + + const articleLikeCounts = articleIds.length + ? await articleLikeRepository.countByTargetIds(articleIds) + : []; + const articleLikeCountMap = Object.fromEntries( + articleLikeCounts.map((al) => [al.articleId, al._count.articleId]) + ); + + const commentLikeCounts = commentIds.length + ? await commentLikeRepository.countByTargetIds(commentIds) + : []; + const commentLikeCountMap = Object.fromEntries( + commentLikeCounts.map((cl) => [cl.commentId, cl._count.commentId]) + ); + + let myLikedArticleIds: number[] = []; + let myLikedCommentIds: number[] = []; + + if (typeof userId === 'number') { + const likedArticles = articleIds.length + ? await articleLikeRepository.countByTargetIds(articleIds) + : []; + myLikedArticleIds = likedArticles.map((l) => l.articleId); + + const likedComments = commentIds.length + ? await commentLikeRepository.findByUserAndTargetIds(userId, commentIds) + : []; + myLikedCommentIds = likedComments.map((l) => l.commentId); + } + + const articlesWithLike = articles.map((a) => ({ + ...a, + likeCount: articleLikeCountMap[a.id] || 0, + isLiked: + typeof userId === 'number' ? myLikedArticleIds.includes(a.id) : false, + comments: a.comments.map((c) => ({ + ...c, + likeCount: commentLikeCountMap[c.id] || 0, + isLiked: + typeof userId === 'number' ? myLikedCommentIds.includes(c.id) : false, + })), + })); + + const totalArticles = await articleRepository.count(where); + const totalPages = Math.ceil(totalArticles / limit); + + return { + data: articlesWithLike, + pagination: { totalArticles, totalPages, currentPage: page, limit }, + }; + } + + // 단일 게시글 조회 + async getArticleById(articleId: number, userId?: number) { + const article = await articleRepository.findUnique(articleId); + + if (!article) throw new AppError('존재하지 않는 게시글입니다.', 404); + + const likeCount = await articleLikeRepository.count(article.id); + const isLiked = userId + ? await articleLikeRepository.exists(userId, article.id) + : false; + + const commentIds = article.comments.map((c) => c.id); + + const commentLikeCounts = await commentLikeRepository.countByTargetIds( + commentIds + ); + const commentLikeCountMap = Object.fromEntries( + commentLikeCounts.map((cl) => [cl.commentId, cl._count.commentId]) + ); + + let myLikedCommentIds: number[] = []; + if (userId) { + const likedComments = await commentLikeRepository.findByUserAndTargetIds( + userId, + commentIds + ); + myLikedCommentIds = likedComments.map((l) => l.commentId); + } + + const commentsWithLikes = article.comments.map((c) => ({ + ...c, + likeCount: commentLikeCountMap[c.id] || 0, + isLiked: userId ? myLikedCommentIds.includes(c.id) : false, + })); + + return { + ...article, + likeCount, + isLiked, + comments: commentsWithLikes, + }; + } + + // 게시글 작성 + async createArticle(title: string, content: string, userId: number) { + if (!userId) throw new AppError('작성자를 확인할 수 없습니다.', 400); + + const newArticle = await articleRepository.create({ + title, + content, + userId, + }); + if (!newArticle) throw new AppError('게시글 등록에 실패했습니다.', 400); + + return newArticle; + } + + // 게시글 수정 + async updateArticle( + articleId: number, + userId: number, + updateData: UpdateArticleDto + ) { + const article = await articleRepository.findUnique(articleId); + if (!article) throw new AppError('게시글 없음', 404); + if (article.userId !== userId) throw new AppError('권한 없음', 403); + + await articleRepository.update(articleId, userId, updateData); + + return { message: '게시글이 수정되었습니다.' }; + } + + // 게시글 삭제 + async deleteArticle(articleId: number, userId: number) { + const deleted = await articleRepository.delete(articleId, userId); + if (deleted.count === 0) { + throw new AppError('권한이 없거나 게시글이 존재하지 않습니다.', 403); + } + return { message: '게시글이 삭제되었습니다.' }; + } + + // 본인이 작성한 게시글 조회 + async getUserArticles(userId: number) { + return articleRepository.findUserArticles(userId); + } + + // 좋아요한 게시글 조회 + async getUserLikedArticles(userId: number) { + return articleRepository.findLikedArticles(userId); + } + + // 게시글 좋아요 + async articleLike(userId: number, articleId: number) { + const alreadyLiked = await articleLikeRepository.exists(userId, articleId); + if (alreadyLiked) { + throw new AppError('이미 좋아요를 눌렀습니다.', 400); + } + await articleLikeRepository.create(userId, articleId); + const count = await articleLikeRepository.count(articleId); + return { message: '좋아요 완료', likeCount: count }; + } + + // 게시글 좋아요 취소 + async articleUnlike(userId: number, articleId: number) { + const exists = await articleLikeRepository.exists(userId, articleId); + if (!exists) { + throw new AppError('좋아요를 누른 기록이 없습니다.', 400); + } + await articleLikeRepository.delete(userId, articleId); + const count = await articleLikeRepository.count(articleId); + return { message: '좋아요 취소', likeCount: count }; + } +} + +export const articleService = new ArticleService(); diff --git a/part4-mission11/src/services/comments/article-comment-service.ts b/part4-mission11/src/services/comments/article-comment-service.ts new file mode 100644 index 000000000..884f22118 --- /dev/null +++ b/part4-mission11/src/services/comments/article-comment-service.ts @@ -0,0 +1,80 @@ +import { commentService } from './comment-service.js'; +import AppError from '../../lib/appError.js'; +import { articleRepository } from '../../repositories/article-repository.js'; +import { articleCommentRepository } from '../../repositories/comments/article-comment-repository.js'; +import { commentLikeRepository } from '../../repositories/like-repository.js'; +import { userRepository } from '../../repositories/user-repository.js'; +import { notificationService } from '../notification-service.js'; + +class ArticleCommentService { + updateComment = commentService.updateComment; + deleteComment = commentService.deleteComment; + commentLike = commentService.likeComment; + commentUnlike = commentService.unlikeComment; + + async getCommentsByArticleId(articleId: number, userId?: number) { + const comments = await articleCommentRepository.findByArticleId(articleId); + if (comments.length === 0) return []; + + const commentIds = comments.map((c) => c.id); + + // 1) 댓글별 likeCount 한 번에 가져오기 + const grouped = await commentLikeRepository.countByTargetIds(commentIds); + const likeCountMap = grouped.reduce>((acc, row) => { + acc[row.commentId] = row._count.commentId; + return acc; + }, {}); + + // 2) 유저가 좋아요 누른 댓글들 한 번에 가져오기 + let likedSet = new Set(); + if (userId) { + const likedRows = await commentLikeRepository.findByUserAndTargetIds( + userId, + commentIds + ); + likedSet = new Set(likedRows.map((row) => row.commentId)); + } + + // 3) 메모리에서 합쳐서 반환 + return comments.map((c) => ({ + ...c, + likeCount: likeCountMap[c.id] ?? 0, + isLiked: userId ? likedSet.has(c.id) : false, + })); + } + + async createArticleComment( + articleId: number, + content: string, + userId: number + ) { + // 1) 글 정보(작성자, 제목) → 레포 + const article = await articleRepository.findLiteById(articleId); + if (!article) throw new AppError('게시글을 찾을 수 없습니다.', 404); + + // 2) 댓글 생성 → 레포 + const comment = await articleCommentRepository.create( + articleId, + content, + userId + ); + + // 3) 자기 글에 본인이 단 댓글은 알림 스킵 + if (article.userId !== userId) { + // 4) 작성자명 → 레포 + const commenter = await userRepository.findUsernameById(userId); + + await notificationService.pushArticleComment({ + receiverUserId: article.userId, + articleId: article.id, + commentId: comment.id, + articleTitle: article.title, + commenterName: commenter?.username ?? '익명', + }); + } + + return comment; + } +} + +export const articleCommentService = new ArticleCommentService(); diff --git a/part4-mission11/src/services/comments/comment-service.ts b/part4-mission11/src/services/comments/comment-service.ts new file mode 100644 index 000000000..e5a8903cc --- /dev/null +++ b/part4-mission11/src/services/comments/comment-service.ts @@ -0,0 +1,45 @@ +import AppError from '../../lib/appError.js'; +import { commentRepository } from '../../repositories/comments/comment-repository.js'; +import { commentLikeRepository } from '../../repositories/like-repository.js'; + +class CommentService { + async updateComment(commentId: number, userId: number, content: string) { + const comment = await commentRepository.findById(commentId); + if (!comment) throw new AppError('댓글을 찾을 수 없습니다.', 404); + if (comment.userId !== userId) throw new AppError('권한이 없습니다.', 403); + + return commentRepository.update(commentId, content); + } + + async deleteComment(commentId: number, userId: number) { + const deleted = await commentRepository.delete(commentId, userId); + if (deleted.count === 0) { + throw new AppError('권한이 없거나 댓글이 존재하지 않습니다.', 403); + } + return { message: '댓글이 삭제되었습니다.' }; + } + + async likeComment(userId: number, commentId: number) { + const exists = await commentLikeRepository.exists(userId, commentId); + if (exists) { + throw new AppError('이미 좋아요를 눌렀습니다.', 400); + } + + await commentLikeRepository.create(userId, commentId); + const count = await commentLikeRepository.count(commentId); + return { message: '좋아요 완료', likeCount: count }; + } + + async unlikeComment(userId: number, commentId: number) { + const exists = await commentLikeRepository.exists(userId, commentId); + if (!exists) { + throw new AppError('좋아요를 누른 기록이 없습니다.', 400); + } + + await commentLikeRepository.delete(userId, commentId); + const count = await commentLikeRepository.count(commentId); + return { message: '좋아요 취소', likeCount: count }; + } +} + +export const commentService = new CommentService(); diff --git a/part4-mission11/src/services/comments/product-comment-service.ts b/part4-mission11/src/services/comments/product-comment-service.ts new file mode 100644 index 000000000..9cef3a054 --- /dev/null +++ b/part4-mission11/src/services/comments/product-comment-service.ts @@ -0,0 +1,79 @@ +import { commentService } from './comment-service.js'; +import AppError from '../../lib/appError.js'; +import { productCommentRepository } from '../../repositories/comments/product-comment-repository.js'; +import { commentLikeRepository } from '../../repositories/like-repository.js'; +import { productRepository } from '../../repositories/product-repository.js'; +import { userRepository } from '../../repositories/user-repository.js'; +import { notificationService } from '../notification-service.js'; + +class ProductCommentService { + updateComment = commentService.updateComment; + deleteComment = commentService.deleteComment; + commentLike = commentService.likeComment; + commentUnlike = commentService.unlikeComment; + + async getCommentsByProductId(productId: number, userId?: number) { + const comments = await productCommentRepository.findByProductId(productId); + if (comments.length === 0) return []; + + const commentIds = comments.map((c) => c.id); + + // 1) 댓글별 likeCount 한 번에 가져오기 + const grouped = await commentLikeRepository.countByTargetIds(commentIds); + const likeCountMap = grouped.reduce>((acc, row) => { + acc[row.commentId] = row._count.commentId; + return acc; + }, {}); + + // 2) 유저가 좋아요 누른 댓글들 한 번에 가져오기 + let likedSet = new Set(); + if (userId) { + const likedRows = await commentLikeRepository.findByUserAndTargetIds( + userId, + commentIds + ); + likedSet = new Set(likedRows.map((row) => row.commentId)); + } + + // 3) 메모리에서 합쳐서 반환 + return comments.map((c) => ({ + ...c, + likeCount: likeCountMap[c.id] ?? 0, + isLiked: userId ? likedSet.has(c.id) : false, + })); + } + + async createProductComment( + productId: number, + content: string, + userId: number + ) { + // 1) 상품 정보(소유자, 이름) + const product = await productRepository.findLiteById(productId); + if (!product) throw new AppError('상품을 찾을 수 없습니다.', 404); + + // 2) 댓글 생성 + const comment = await productCommentRepository.create( + productId, + content, + userId + ); + + // 3) 자기 상품에 본인이 단 댓글은 알림 스킵 + if (product.userId !== userId) { + const commenter = await userRepository.findUsernameById(userId); + + await notificationService.pushProductComment({ + receiverUserId: product.userId, + productId: product.id, + commentId: comment.id, + productName: product.name, + commenterName: commenter?.username ?? '익명', + }); + } + + return comment; + } +} + +export const productCommentService = new ProductCommentService(); diff --git a/part4-mission11/src/services/notification-service.ts b/part4-mission11/src/services/notification-service.ts new file mode 100644 index 000000000..a8567ae9a --- /dev/null +++ b/part4-mission11/src/services/notification-service.ts @@ -0,0 +1,125 @@ +import { ports } from '../lib/ports.js'; +import { notificationRepository } from '../repositories/notification-repository.js'; + +class NotificationService { + async pushPriceChange(args: { + receiverUserId: number; + productId: number; + productName: string; + oldPrice: number; + newPrice: number; + }) { + const { receiverUserId, productId, productName, oldPrice, newPrice } = args; + + const message = `${productName} 가격이 ${oldPrice}원 → ${newPrice}원 으로 변경되었습니다.`; + + const notif = await notificationRepository.create({ + userId: receiverUserId, + type: 'PRICE_CHANGE', + message, + productId, + }); + + // 객체 1개로 전달 + data는 있을 때만 포함 + ports.ws.notifyUser({ + userId: receiverUserId, + type: notif.type, + message: notif.message, + createdAt: notif.createdAt, + ...(notif.productId !== null + ? { data: { productId: notif.productId } } + : {}), + }); + + return notif; + } + + async pushArticleComment(args: { + receiverUserId: number; + articleId: number; + commentId: number; + articleTitle: string; + commenterName: string; + }) { + const { + receiverUserId, + articleId, + commentId, + articleTitle, + commenterName, + } = args; + + const message = `${commenterName} 님이 '${articleTitle}' 글에 댓글을 남겼습니다.`; + + const notif = await notificationRepository.create({ + userId: receiverUserId, + type: 'NEW_COMMENT', + message, + articleId, + commentId, + }); + + // 객체 1개로 전달 + data는 있을 때만 포함 + ports.ws.notifyUser({ + userId: receiverUserId, + type: notif.type, + message: notif.message, + createdAt: notif.createdAt, + ...(notif.articleId !== null && notif.commentId !== null + ? { data: { articleId: notif.articleId, commentId: notif.commentId } } + : {}), + }); + + return notif; + } + + async pushProductComment(args: { + receiverUserId: number; + productId: number; + commentId: number; + productName: string; + commenterName: string; + }) { + const { receiverUserId, productId, commentId, productName, commenterName } = + args; + const message = `${commenterName} 님이 상품 '${productName}'에 댓글을 남겼습니다.`; + + const notif = await notificationRepository.create({ + userId: receiverUserId, + type: 'NEW_COMMENT', + message, + productId, + commentId, + }); + + ports.ws.notifyUser({ + userId: receiverUserId, + type: notif.type, + message: notif.message, + createdAt: notif.createdAt, + ...(notif.productId !== null && notif.commentId !== null + ? { data: { productId: notif.productId, commentId: notif.commentId } } + : {}), + }); + + return notif; + } + + async getMyNotifications(userId: number) { + return notificationRepository.findByUserId(userId); + } + + async getMyUnreadCount(userId: number) { + return notificationRepository.countUnread(userId); + } + + async markAsRead(userId: number, notificationId: number) { + await notificationRepository.markAsRead(userId, notificationId); + } + + async markAllAsRead(userId: number) { + await notificationRepository.markAllAsRead(userId); + } +} + +export const notificationService = new NotificationService(); diff --git a/part4-mission11/src/services/product-service.ts b/part4-mission11/src/services/product-service.ts new file mode 100644 index 000000000..e3593233f --- /dev/null +++ b/part4-mission11/src/services/product-service.ts @@ -0,0 +1,267 @@ +import { Prisma } from '@prisma/client'; + +import { notificationService } from './notification-service.js'; +import type { ProductQuery, UpdateProductDto } from '../dtos/product-dto.js'; +import AppError from '../lib/appError.js'; +import { prisma } from '../lib/prismaClient.js'; +import { + productLikeRepository, + commentLikeRepository, +} from '../repositories/like-repository.js'; +import { productRepository } from '../repositories/product-repository.js'; + +class ProductService { + // 전체 상품 조회 + async getAllProducts(query: ProductQuery, userId?: number) { + const page = Number(query.page) || 1; + const limit = Number(query.limit) || 10; + const skip = (page - 1) * limit; + const sort = query.sort === 'old' ? 'old' : 'recent'; + const search = (query.keyword ?? query.query ?? query.search ?? '').trim(); + + const orderBy: Prisma.ProductOrderByWithRelationInput = + sort === 'old' ? { createdAt: 'asc' } : { createdAt: 'desc' }; + + const where: Prisma.ProductWhereInput = search + ? { + OR: [ + { name: { contains: search, mode: 'insensitive' } }, + { description: { contains: search, mode: 'insensitive' } }, + ], + } + : {}; + + const products = await productRepository.findMany({ + skip, + take: limit, + where, + orderBy, + include: { + user: { select: { username: true } }, + comments: { + include: { user: { select: { username: true } } }, + }, + }, + }); + + const productIds = products.map((p) => p.id); + const commentIds = products.flatMap((p) => p.comments.map((c) => c.id)); + + const productLikeCounts = productIds.length + ? await productLikeRepository.countByTargetIds(productIds) + : []; + const productLikeCountMap = Object.fromEntries( + productLikeCounts.map((pl) => [pl.productId, pl._count.productId]) + ); + + const commentLikeCounts = commentIds.length + ? await commentLikeRepository.countByTargetIds(commentIds) + : []; + const commentLikeCountMap = Object.fromEntries( + commentLikeCounts.map((cl) => [cl.commentId, cl._count.commentId]) + ); + + let myLikedProductIds: number[] = []; + let myLikedCommentIds: number[] = []; + + if (typeof userId === 'number') { + const likedProducts = productIds.length + ? await productLikeRepository.findByUserAndTargetIds(userId, productIds) + : []; + myLikedProductIds = likedProducts.map((l) => l.productId); + + const likedComments = commentIds.length + ? await commentLikeRepository.findByUserAndTargetIds(userId, commentIds) + : []; + myLikedCommentIds = likedComments.map((l) => l.commentId); + } + + const productsWithLike = products.map((p) => ({ + ...p, + likeCount: productLikeCountMap[p.id] || 0, + isLiked: + typeof userId === 'number' ? myLikedProductIds.includes(p.id) : false, + comments: p.comments.map((c) => ({ + ...c, + likeCount: commentLikeCountMap[c.id] || 0, + isLiked: + typeof userId === 'number' ? myLikedCommentIds.includes(c.id) : false, + })), + })); + + const totalProducts = await productRepository.count(where); + const totalPages = Math.ceil(totalProducts / limit); + + return { + data: productsWithLike, + pagination: { totalProducts, totalPages, currentPage: page, limit }, + }; + } + + // 단일 상품 조회 + async getProductById(productId: number, userId?: number) { + const product = await productRepository.findUnique(productId, userId); + + if (!product) throw new AppError('존재하지 않는 상품입니다.', 404); + + const likeCount = await productLikeRepository.count(product.id); + const isLiked = userId + ? await productLikeRepository.exists(userId, product.id) + : false; + + const commentIds = product.comments.map((c) => c.id); + + const commentLikeCounts = await commentLikeRepository.countByTargetIds( + commentIds + ); + const commentLikeCountMap = Object.fromEntries( + commentLikeCounts.map((cl) => [cl.commentId, cl._count.commentId]) + ); + + let myLikedCommentIds: number[] = []; + if (userId) { + const likedComments = await commentLikeRepository.findByUserAndTargetIds( + userId, + commentIds + ); + myLikedCommentIds = likedComments.map((l) => l.commentId); + } + + const commentsWithLikes = product.comments.map((c) => ({ + ...c, + likeCount: commentLikeCountMap[c.id] || 0, + isLiked: userId ? myLikedCommentIds.includes(c.id) : false, + })); + + const productWithLike = { + ...product, + likeCount, + isLiked, + comments: commentsWithLikes, + }; + + return productWithLike; + } + + async createProduct( + userId: number, + name: string, + description: string, + price: number, + tags: string[] + ) { + const newProduct = await productRepository.create({ + userId, + name, + description, + price, + tags, + }); + if (!newProduct) throw new AppError('제품 등록 실패', 400); + return newProduct; + } + + async updateProductPrice( + productId: number, + newPrice: number, + _actorUserId: number + ) { + // 1. 기존 상품 + const product = await productRepository.findUnique(productId); + if (!product) { + throw new AppError('존재하지 않는 상품입니다.', 404); + } + + // 2. 가격이 같으면 그냥 반환 + if (product.price === newPrice) { + return product; + } + + // 3. 트랜잭션: 가격 업데이트 + 좋아요 유저 ID 조회 + const { updatedProduct, likedUserIds } = await prisma.$transaction( + async (tx: Prisma.TransactionClient) => { + const updatedProduct = await productRepository.updatePriceTx( + tx, + productId, + newPrice + ); + + const likedUserIds = + await productLikeRepository.findUserIdsWhoLikedProductTx( + tx, + productId + ); + + return { updatedProduct, likedUserIds }; + } + ); + + // 4. 트랜잭션 이후 → 알림 발송 (기존 서비스 그대로 사용) + for (const userId of likedUserIds) { + await notificationService.pushPriceChange({ + receiverUserId: userId, + productId, + oldPrice: product.price, + newPrice, + productName: product.name, + }); + } + + return updatedProduct; + } + + async updateProduct( + productId: number, + userId: number, + updateData: UpdateProductDto + ) { + const product = await productRepository.findUnique(productId); + if (!product) throw new AppError('제품 없음', 404); + if (product.userId !== userId) throw new AppError('권한 없음', 403); + + await productRepository.update(productId, userId, updateData); + + return { message: '상품이 수정되었습니다.' }; + } + + async deleteProduct(productId: number, userId: number) { + const deleted = await productRepository.delete(productId, userId); + if (deleted.count === 0) throw new AppError('권한 없거나 제품 없음', 403); + return { message: '제품 삭제 완료' }; + } + + // --------------------------- + // 본인 상품 조회 + async getUserProducts(userId: number) { + return productRepository.findUserProducts(userId); + } + + // 좋아요한 상품 조회 + async getUserLikedProducts(userId: number) { + return productRepository.findLikedProducts(userId); + } + + // 상품 좋아요 + async productLike(userId: number, productId: number) { + const alreadyLiked = await productLikeRepository.exists(userId, productId); + if (alreadyLiked) { + throw new AppError('이미 좋아요를 눌렀습니다.', 400); + } + await productLikeRepository.create(userId, productId); + const count = await productLikeRepository.count(productId); + return { message: '좋아요 완료', likeCount: count }; + } + + // 상품 좋아요 취소 + async productUnlike(userId: number, productId: number) { + const exists = await productLikeRepository.exists(userId, productId); + if (!exists) { + throw new AppError('좋아요를 누른 기록이 없습니다.', 400); + } + await productLikeRepository.delete(userId, productId); + const count = await productLikeRepository.count(productId); + return { message: '좋아요 취소', likeCount: count }; + } +} + +export const productService = new ProductService(); diff --git a/part4-mission11/src/services/user-service.ts b/part4-mission11/src/services/user-service.ts new file mode 100644 index 000000000..892e0014f --- /dev/null +++ b/part4-mission11/src/services/user-service.ts @@ -0,0 +1,122 @@ +import bcrypt from 'bcrypt'; +import type { Response } from 'express'; + +import type { UserPublic } from '../dtos/user-dto.js'; +import AppError from '../lib/appError.js'; +import { + NODE_ENV, + ACCESS_TOKEN_COOKIE_NAME, + REFRESH_TOKEN_COOKIE_NAME, +} from '../lib/constants.js'; +import { exclude } from '../lib/exclude.js'; +import { generateTokens, verifyRefreshToken } from '../lib/token.js'; +import { commentLikeRepository } from '../repositories/like-repository.js'; +import { userRepository } from '../repositories/user-repository.js'; + +class UserService { + async register(username: string, email: string, password: string) { + const existing = await userRepository.findByUsername(username); + if (existing) throw new AppError('이미 사용 중인 닉네임입니다.', 409); + + const hashed = await bcrypt.hash(password, 10); + const user = await userRepository.createUser({ + username, + email, + password: hashed, + }); + + return exclude(user, ['password']); + } + + async login(userId: number) { + return generateTokens(userId); + } + + async getUserProfile(userId: number) { + return userRepository.findById(userId); + } + + async updateUserProfile(userId: number, updateData: UserPublic) { + return userRepository.updateUser(userId, updateData); + } + + async updatePassword( + userId: number, + currentPassword: string, + newPassword: string + ) { + const user = await userRepository.findByIdWithPassword(userId); + if (!user) throw new AppError('사용자를 찾을 수 없습니다.', 404); + + // 현재 비밀번호 비교 + const isValid = await bcrypt.compare(currentPassword, user.password); + if (!isValid) { + throw new AppError('현재 비밀번호가 일치하지 않습니다.', 400); + } + + // 기존 비밀번호와 동일한지 확인 + const isSame = await bcrypt.compare(newPassword, user.password); + if (isSame) { + throw new AppError('기존 비밀번호로는 변경할 수 없습니다.', 400); + } + + // 새 비밀번호로 업데이트 + const hashed = await bcrypt.hash(newPassword, 10); + const updated = await userRepository.updatePassword(userId, hashed); + + return exclude(updated, ['password']); + } + + async getUserComments(userId: number) { + const comments = await userRepository.getUserComments(userId); + + return Promise.all( + comments.map(async (c) => { + const likeCount = await commentLikeRepository.count(c.id); + return { ...c, likeCount }; + }) + ); + } + + async getUserLikedComments(userId: number) { + const liked = await userRepository.getUserLikedComments(userId); + + return Promise.all( + liked.map(async (like) => { + const c = like.comment; + const likeCount = await commentLikeRepository.count(c.id); + return { ...c, likeCount, isLiked: true }; + }) + ); + } + + setTokenCookies(res: Response, accessToken: string, refreshToken: string) { + res.cookie(ACCESS_TOKEN_COOKIE_NAME, accessToken, { + httpOnly: true, + secure: NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 1 * 60 * 60 * 1000, + }); + res.cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken, { + httpOnly: true, + secure: NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 7 * 24 * 60 * 60 * 1000, + path: '/auth/refresh', + }); + } + + clearTokenCookies(res: Response) { + res.clearCookie(ACCESS_TOKEN_COOKIE_NAME); + res.clearCookie(REFRESH_TOKEN_COOKIE_NAME); + } + + async refreshTokens(refreshToken: string, res: Response) { + const { userId } = verifyRefreshToken(refreshToken); + const tokens = generateTokens(userId); + this.setTokenCookies(res, tokens.accessToken, tokens.refreshToken); + return tokens.accessToken; + } +} + +export const userService = new UserService(); diff --git a/part4-mission11/src/swagger.ts b/part4-mission11/src/swagger.ts new file mode 100644 index 000000000..81608cdf2 --- /dev/null +++ b/part4-mission11/src/swagger.ts @@ -0,0 +1,21 @@ +import type { Express } from 'express'; +import swaggerJSDoc from 'swagger-jsdoc'; +import swaggerUi from 'swagger-ui-express'; + +const options: swaggerJSDoc.Options = { + definition: { + openapi: '3.0.0', + info: { + title: 'My API', + version: '1.0.0', + description: 'Express + TypeScript API 문서', + }, + }, + apis: ['./src/routes/*.ts', './src/controllers/*.ts'], +}; + +const swaggerSpec = swaggerJSDoc(options); + +export function setupSwagger(app: Express) { + app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); +} diff --git a/part4-mission11/src/types/authenticated-request.ts b/part4-mission11/src/types/authenticated-request.ts new file mode 100644 index 000000000..fd1f2f131 --- /dev/null +++ b/part4-mission11/src/types/authenticated-request.ts @@ -0,0 +1,11 @@ +import type { Request } from 'express'; + +export interface AuthUser { + id: number; + username: string; + email: string; +} + +export interface AuthenticatedRequest extends Request { + user: AuthUser; +} diff --git a/part4-mission11/src/types/express.d.ts b/part4-mission11/src/types/express.d.ts new file mode 100644 index 000000000..75b17422e --- /dev/null +++ b/part4-mission11/src/types/express.d.ts @@ -0,0 +1,15 @@ +import express from 'express'; + +declare global { + namespace Express { + interface User { + id: number; + username?: string; + email: string; + } + + interface Request { + user?: User; + } + } +} diff --git a/part4-mission11/src/types/multer-s3.d.ts b/part4-mission11/src/types/multer-s3.d.ts new file mode 100644 index 000000000..6ff5c2320 --- /dev/null +++ b/part4-mission11/src/types/multer-s3.d.ts @@ -0,0 +1,12 @@ +export {}; + +declare global { + namespace Express { + namespace Multer { + interface File { + location?: string; + key?: string; + } + } + } +} diff --git a/part4-mission11/src/types/notification.ts b/part4-mission11/src/types/notification.ts new file mode 100644 index 000000000..fb6da2438 --- /dev/null +++ b/part4-mission11/src/types/notification.ts @@ -0,0 +1,15 @@ +import type { NotificationType as PrismaNotificationType } from '@prisma/client'; +import { NotificationType as NotificationTypeValues } from '@prisma/client'; + +export type NotificationType = PrismaNotificationType; + +export { NotificationTypeValues }; + +export type NotificationCreateInput = { + userId: number; + type: NotificationType; + message: string; + productId?: number | null; + articleId?: number | null; + commentId?: number | null; +}; diff --git a/part4-mission11/tests/_helper/debug.ts b/part4-mission11/tests/_helper/debug.ts new file mode 100644 index 000000000..6e6e08f1b --- /dev/null +++ b/part4-mission11/tests/_helper/debug.ts @@ -0,0 +1,6 @@ +export const dbg = (...args: unknown[]) => { + if (process.env.NODE_ENV === 'test') console.log('[DBG]', ...args); +}; +export const dbe = (...args: unknown[]) => { + if (process.env.NODE_ENV === 'test') console.error('[DBG:ERR]', ...args); +}; diff --git a/part4-mission11/tests/_helper/factories.ts b/part4-mission11/tests/_helper/factories.ts new file mode 100644 index 000000000..4963f2ebb --- /dev/null +++ b/part4-mission11/tests/_helper/factories.ts @@ -0,0 +1,116 @@ +import type { NotificationType } from '../../src/types/notification.js'; + +export type NotificationRecord = { + id: number; + userId: number; + articleId: number | null; + productId: number | null; + commentId: number | null; + type: NotificationType; + message: string; + isRead: boolean; + createdAt: Date; +}; + +export function makeNotification( + over: Partial = {} +): NotificationRecord { + return { + id: 1, + userId: 1, + articleId: null, + productId: null, + commentId: null, + type: 'NEW_COMMENT' as NotificationType, + message: '알림', + isRead: false, + createdAt: new Date(), + ...over, + }; +} + +export type ArticleLite = { + id: number; + title: string; + userId: number; +}; + +export type ProductLite = { + id: number; + name: string; + userId: number; +}; + +export type CommentCore = { + id: number; + content: string; + userId: number; + user: { username: string }; + articleId: number | null; + productId: number | null; + createdAt: Date; + updatedAt: Date; +}; + +// 리스트 조회에서 사용하는 셀렉트 결과 타입 +export type ListedComment = { + id: number; + content: string; + createdAt: Date; + updatedAt: Date; + user: { username: string }; +}; + +export type CommentLikeRecord = { + id: number; + userId: number; + commentId: number; + createdAt: Date; +}; + +export function makeArticleLite(over?: Partial): ArticleLite { + return { id: 1, title: 'Title', userId: 10, ...over }; +} + +export function makeProductLite(over?: Partial): ProductLite { + return { id: 1, name: 'Prod', userId: 20, ...over }; +} + +export function makeComment(over?: Partial): CommentCore { + return { + id: 100, + content: 'hello', + userId: 999, + user: { username: 'user' }, + articleId: null, + productId: null, + createdAt: new Date(), + updatedAt: new Date(), + ...over, + }; +} + +export function makeListedComment( + over?: Partial +): ListedComment { + return { + id: 200, + content: 'listed', + createdAt: new Date(), + updatedAt: new Date(), + user: { username: 'user' }, + ...over, + }; +} + +export function makeCommentLike( + over: Partial = {} +): CommentLikeRecord { + return { + id: 1, + userId: 1, + commentId: 1, + createdAt: new Date(), + ...over, + }; +} diff --git a/part4-mission11/tests/_helper/jest-typed.ts b/part4-mission11/tests/_helper/jest-typed.ts new file mode 100644 index 000000000..8c6f06d86 --- /dev/null +++ b/part4-mission11/tests/_helper/jest-typed.ts @@ -0,0 +1,17 @@ +export type Awaited = T extends Promise ? U : T; + +export function asMockFn( + fn: (args: TArgs) => Promise +) { + return fn as jest.MockedFunction<(args: TArgs) => Promise>; +} + +/** 공통 함수 시그니처 */ +type FnLike = (...args: unknown[]) => unknown; + +/** 새 범용 캐스터: 인자 0개/여러 개/옵셔널 모두 커버 */ +export type MockedFn = jest.MockedFunction; + +export function asMock(fn: Fn) { + return fn as jest.MockedFunction; +} diff --git a/part4-mission11/tests/_helper/jest.env.setup.ts b/part4-mission11/tests/_helper/jest.env.setup.ts new file mode 100644 index 000000000..94c7328da --- /dev/null +++ b/part4-mission11/tests/_helper/jest.env.setup.ts @@ -0,0 +1,11 @@ +process.env.NODE_ENV ??= 'test'; + +process.env.ACCESS_TOKEN_SECRET ??= 'test_access'; +process.env.REFRESH_TOKEN_SECRET ??= 'test_refresh'; +process.env.ACCESS_TOKEN_COOKIE_NAME ??= 'access-token'; +process.env.REFRESH_TOKEN_COOKIE_NAME ??= 'refresh-token'; +process.env.ACCESS_TOKEN_EXPIRES_IN ??= '15m'; +process.env.REFRESH_TOKEN_EXPIRES_IN ??= '7d'; + +process.env.RESEND_API_KEY ??= 'dummy'; +process.env.DATABASE_URL ??= 'postgresql://user:pass@localhost:5432/dbname'; diff --git a/part4-mission11/tests/_helper/mock-modules.ts b/part4-mission11/tests/_helper/mock-modules.ts new file mode 100644 index 000000000..97f6f937b --- /dev/null +++ b/part4-mission11/tests/_helper/mock-modules.ts @@ -0,0 +1,26 @@ +import { prisma } from './prisma-mock.js'; + +export async function setupPrismaMock() { + jest.resetModules(); + // @ts-expect-error: unstable_mockModule은 타입 선언이 없음 (런타임 전용) + await jest.unstable_mockModule('../../lib/prismaClient.js', () => ({ + __esModule: true, + prisma, + default: prisma, + })); +} + +// 필요할 때만 추가 모듈 주입 +/* +await jest.unstable_mockModule('../../lib/token.js', () => ({ + __esModule: true, + generateTokens: jest.fn().mockReturnValue({ accessToken: 'acc', refreshToken: 'ref' }), + verifyRefreshToken: jest.fn().mockReturnValue({ userId: 7 }), +})); +await jest.unstable_mockModule('../../lib/constants.js', () => ({ + __esModule: true, + NODE_ENV: 'test', + ACCESS_TOKEN_COOKIE_NAME: 'AT', + REFRESH_TOKEN_COOKIE_NAME: 'RT', +})); +*/ diff --git a/part4-mission11/tests/_helper/prisma-mock.ts b/part4-mission11/tests/_helper/prisma-mock.ts new file mode 100644 index 000000000..7e4126926 --- /dev/null +++ b/part4-mission11/tests/_helper/prisma-mock.ts @@ -0,0 +1,1566 @@ +//#region Imports +import { jest } from '@jest/globals'; +import bcrypt from 'bcrypt'; +//#endregion + +//#region Records (Types) +export type UserRecord = { + id: number; + username: string; + email: string; + password: string; + images: string[]; + createdAt: Date; + updatedAt: Date; +}; + +export type ArticleRecord = { + id: number; + title: string; + content: string; + userId: number; + tags: string[]; + images: string[]; + createdAt: Date; + updatedAt: Date; +}; + +export type ProductRecord = { + id: number; + name: string; + description: string; + price: number; + userId: number; + tags: string[]; + images: string[]; + createdAt: Date; + updatedAt: Date; +}; + +export type ArticleLikeRecord = { + id: number; + articleId: number; + userId: number; +}; + +export type ProductLikeRecord = { + id: number; + productId: number; + userId: number; +}; + +export type CommentRecord = { + id: number; + content: string; + userId: number; + articleId?: number | null; + productId?: number | null; + createdAt: Date; + updatedAt: Date; +}; + +export type NotificationRecord = { + id: number; + userId: number; + type: 'COMMENT' | 'LIKE' | 'SYSTEM' | string; + message: string; + isRead: boolean; + createdAt: Date; + updatedAt: Date; +}; + +type CommentLikeRecord = { id: number; commentId: number; userId: number }; + +type BooleanSelect = Partial>; + +type UserSelect = BooleanSelect; +type UserWhereUnique = { id?: number; username?: string; email?: string }; + +type FindUniqueUserArgs = + | { where: UserWhereUnique; select?: undefined } + | { + where: UserWhereUnique; + select: Pick; + }; + +type FindUniqueUserReturn = A extends { + select: infer S; +} + ? S extends UserSelect + ? Pick> | null + : UserRecord | null + : UserRecord | null; + +type ProductSelect = BooleanSelect; +type FindUniqueProductArgs = + | { where: { id: number }; select?: undefined } + | { + where: { id: number }; + select: Pick; + }; + +type FindUniqueProductReturn = A extends { + select: infer S; +} + ? S extends ProductSelect + ? Pick> | null + : ProductRecord | null + : ProductRecord | null; +//#endregion + +//#region In-memory DB & Sequences +const db: { + users: UserRecord[]; + articles: ArticleRecord[]; + products: ProductRecord[]; + comments: CommentRecord[]; + articleLikes: ArticleLikeRecord[]; + productLikes: ProductLikeRecord[]; + commentLikes: CommentLikeRecord[]; + notifications: NotificationRecord[]; +} = { + users: [], + articles: [], + products: [], + comments: [], + articleLikes: [], + productLikes: [], + commentLikes: [], + notifications: [], +}; + +let seq = { + user: 1, + article: 1, + product: 1, + cmt: 1, + aLike: 1, + pLike: 1, + cLike: 1, + notif: 1, +}; +//#endregion + +//#region Utils (helpers) +function pickUserBy(where: UserWhereUnique): UserRecord | undefined { + const { id, username, email } = where; + return db.users.find( + (u) => + (id !== undefined && u.id === id) || + (username !== undefined && u.username === username) || + (email !== undefined && u.email === email) + ); +} + +function ensureArticleArrays< + T extends { + tags?: unknown; + images?: unknown; + comments?: unknown; + likes?: unknown; + } +>(a: T) { + return { + ...a, + tags: Array.isArray(a.tags) ? a.tags : [], + images: Array.isArray(a.images) ? a.images : [], + comments: Array.isArray(a.comments) ? a.comments : [], + likes: Array.isArray(a.likes) ? a.likes : [], + }; +} +function ensureProductArrays< + T extends { + tags?: unknown; + images?: unknown; + comments?: unknown; + likes?: unknown; + } +>(p: T) { + return { + ...p, + tags: Array.isArray(p.tags) ? p.tags : [], + images: Array.isArray(p.images) ? p.images : [], + comments: Array.isArray(p.comments) ? p.comments : [], + likes: Array.isArray(p.likes) ? p.likes : [], + }; +} + +function isNumArray(x: unknown): x is number[] { + return ( + Array.isArray(x) && + x.every((n) => typeof n === 'number' && Number.isFinite(n)) + ); +} + +function wantsCount(v: unknown, key: string): boolean { + if (typeof v !== 'object' || v === null) return false; + const obj = v as Record; + return obj._all === true || obj[key] === true; +} + +type InsensitiveMode = 'insensitive' | 'default'; + +function matchesArticleWhere( + a: ArticleRecord, + where?: { + OR?: Array< + | { title?: { contains?: string; mode?: InsensitiveMode } } + | { content?: { contains?: string; mode?: InsensitiveMode } } + >; + } +): boolean { + if (!where?.OR || where.OR.length === 0) return true; + return where.OR.some((cond) => { + if ('title' in cond && cond.title?.contains != null) { + const q = cond.title.contains!; + return cond.title.mode === 'insensitive' + ? a.title.toLowerCase().includes(q.toLowerCase()) + : a.title.includes(q); + } + if ('content' in cond && cond.content?.contains != null) { + const q = cond.content.contains!; + return cond.content.mode === 'insensitive' + ? a.content.toLowerCase().includes(q.toLowerCase()) + : a.content.includes(q); + } + return false; + }); +} + +function matchesProductWhere( + p: ProductRecord, + where?: { + OR?: Array< + | { name?: { contains?: string; mode?: InsensitiveMode } } + | { description?: { contains?: string; mode?: InsensitiveMode } } + >; + } +): boolean { + if (!where?.OR || where.OR.length === 0) return true; + return where.OR.some((cond) => { + if ('name' in cond && cond.name?.contains != null) { + const q = cond.name.contains!; + return cond.name.mode === 'insensitive' + ? p.name.toLowerCase().includes(q.toLowerCase()) + : p.name.includes(q); + } + if ('description' in cond && cond.description?.contains != null) { + const q = cond.description.contains!; + return cond.description.mode === 'insensitive' + ? p.description.toLowerCase().includes(q.toLowerCase()) + : p.description.includes(q); + } + return false; + }); +} + +/** 키쌍(where: userId_articleId | articleId_userId) 타입 생성기 */ +type PairKey = + | { [K in `${T1}_${T2}`]: { [P in T1 | T2]: number } }[`${T1}_${T2}`] + | { [K in `${T2}_${T1}`]: { [P in T1 | T2]: number } }[`${T2}_${T1}`]; + +function pickArticleLikeKey(where: PairKey<'userId', 'articleId'>): { + userId: number; + articleId: number; +} { + const a = + (where as unknown as Record)[ + 'userId_articleId' + ] ?? + (where as unknown as Record)[ + 'articleId_userId' + ]; + if (!a) + throw new Error( + 'articleLike.where needs userId_articleId or articleId_userId' + ); + return { userId: Number(a.userId), articleId: Number(a.articleId) }; +} + +function pickProductLikeKey(where: PairKey<'userId', 'productId'>): { + userId: number; + productId: number; +} { + const a = + (where as unknown as Record)[ + 'userId_productId' + ] ?? + (where as unknown as Record)[ + 'productId_userId' + ]; + if (!a) + throw new Error( + 'productLike.where needs userId_productId or productId_userId' + ); + return { userId: Number(a.userId), productId: Number(a.productId) }; +} + +/* ---- like matchers ---- */ +type ArticleLikeWhere = + | { articleId: number; userId?: number } + | { articleId: { in: number[] }; userId?: number }; +type ProductLikeWhere = + | { productId: number; userId?: number } + | { productId: { in: number[] }; userId?: number }; +type CommentLikeWhere = + | { commentId: number; userId?: number } + | { commentId: { in: number[] }; userId?: number }; + +function matchArticleLike( + where: ArticleLikeWhere, + rec: ArticleLikeRecord +): boolean { + const idOk = + typeof (where as { articleId: number }).articleId === 'number' + ? rec.articleId === (where as { articleId: number }).articleId + : (where as { articleId: { in: number[] } }).articleId.in.includes( + rec.articleId + ); + const userOk = + (where as { userId?: number }).userId === undefined || + rec.userId === (where as { userId?: number }).userId; + return idOk && userOk; +} + +function matchProductLike( + where: ProductLikeWhere, + rec: ProductLikeRecord +): boolean { + const idOk = + typeof (where as { productId: number }).productId === 'number' + ? rec.productId === (where as { productId: number }).productId + : (where as { productId: { in: number[] } }).productId.in.includes( + rec.productId + ); + const userOk = + (where as { userId?: number }).userId === undefined || + rec.userId === (where as { userId?: number }).userId; + return idOk && userOk; +} + +function matchCommentLike( + where: CommentLikeWhere, + rec: CommentLikeRecord +): boolean { + const idOk = + typeof (where as { commentId: number }).commentId === 'number' + ? rec.commentId === (where as { commentId: number }).commentId + : (where as { commentId: { in: number[] } }).commentId.in.includes( + rec.commentId + ); + + const userOk = + (where as { userId?: number }).userId === undefined || + rec.userId === (where as { userId?: number }).userId; + + return idOk && userOk; +} +//#endregion + +//#region Query arg helper types +type OrderDir = 'asc' | 'desc'; + +type ProductWhereOr = { + OR?: Array< + | { name?: { contains?: string; mode?: InsensitiveMode } } + | { description?: { contains?: string; mode?: InsensitiveMode } } + >; +}; +type FindManyProductArgs = { where?: ProductWhereOr }; + +type CommentLikeOrderBy = { id?: OrderDir }; + +type CommentIncludeArticle = + | boolean + | { select?: Partial> }; +type CommentIncludeProduct = + | boolean + | { select?: Partial> }; + +type CommentInclude = + | boolean + | { + select?: Partial> & { + article?: CommentIncludeArticle; + product?: CommentIncludeProduct; + }; + include?: { + article?: CommentIncludeArticle; + product?: CommentIncludeProduct; + }; + }; + +type FindManyCommentLikeArgs = { + where?: { + userId?: number; + commentId?: number | { in?: number[] }; + }; + orderBy?: CommentLikeOrderBy; + skip?: number; + take?: number; + include?: { comment?: CommentInclude }; +}; + +type GroupByCountOpt = { + _count?: { _all?: true } & Partial>; +}; +type ArticleLikeGroupByArgs = { + by?: string[]; + where?: { articleId?: { in?: number[] } }; +} & GroupByCountOpt<'articleId'>; +type ProductLikeGroupByArgs = { + by?: string[]; + where?: { productId?: { in?: number[] } }; +} & GroupByCountOpt<'productId'>; +type CommentLikeGroupByArgs = { + by?: string[]; + where?: { commentId?: { in?: number[] } }; +} & GroupByCountOpt<'commentId'>; +//#endregion + +//#region Prisma Mock Root +export const prisma = { + //#region prisma.user + user: { + create: jest.fn( + async (args: { + data: Omit; + }) => { + const now = new Date(); + const { images: imagesIn, ...rest } = args.data; + const rec: UserRecord = { + id: seq.user++, + createdAt: now, + updatedAt: now, + ...rest, + images: imagesIn ?? [], + }; + db.users.push(rec); + return { ...rec }; + } + ), + + findUnique: jest.fn( + async ( + args: A + ): Promise> => { + const found = pickUserBy(args.where); + if (!found) return null as FindUniqueUserReturn; + if (!('select' in args) || !args.select) + return { ...found } as FindUniqueUserReturn; + + const sel = args.select as UserSelect; + const pickedEntries = Object.keys(sel) + .filter((k) => sel[k as keyof UserSelect]) + .map((k) => [k, found[k as keyof UserRecord]] as const); + const picked = Object.fromEntries(pickedEntries) as Pick< + UserRecord, + Extract + >; + return picked as FindUniqueUserReturn; + } + ), + + update: jest.fn( + async (args: { + where: { id: number }; + data: Partial>; + }) => { + const target = db.users.find((u) => u.id === args.where.id); + if (!target) { + const e = new Error('Not found') as Error & { code: string }; + e.code = 'P2025'; + throw e; + } + Object.assign(target, args.data, { updatedAt: new Date() }); + return { ...target }; + } + ), + }, + //#endregion + + //#region prisma.article + article: { + findMany: jest.fn( + async (args?: { + where?: { + OR?: Array< + | { + title?: { contains?: string; mode?: 'insensitive' | 'default' }; + } + | { + content?: { + contains?: string; + mode?: 'insensitive' | 'default'; + }; + } + >; + }; + orderBy?: { createdAt?: 'asc' | 'desc'; id?: 'asc' | 'desc' }; + skip?: number; + take?: number; + }) => { + let list = db.articles.filter((a) => + matchesArticleWhere(a, args?.where) + ); + if (args?.orderBy?.createdAt || args?.orderBy?.id) { + const dirCreated = args?.orderBy?.createdAt === 'asc' ? 1 : -1; + const dirId = args?.orderBy?.id === 'asc' ? 1 : -1; + list.sort((a, b) => { + if (args?.orderBy?.createdAt) { + const diff = + (a.createdAt.getTime() - b.createdAt.getTime()) * dirCreated; + if (diff !== 0) return diff; + } + if (args?.orderBy?.id) { + return (a.id - b.id) * dirId; + } + return 0; + }); + } + const start = args?.skip ?? 0; + const end = args?.take != null ? start + args.take : undefined; + const sliced = list.slice(start, end); + + return sliced.map((a) => ensureArticleArrays({ ...a })); + } + ), + + findUnique: jest.fn(async (args: { where: { id: number } }) => { + const f = db.articles.find((a) => a.id === args.where.id); + return f ? ensureArticleArrays({ ...f }) : null; + }), + + count: jest.fn( + async (args?: { + where?: { + OR?: Array< + | { + title?: { contains?: string; mode?: InsensitiveMode }; + } + | { + content?: { + contains?: string; + mode?: InsensitiveMode; + }; + } + >; + }; + }) => + db.articles.filter((a) => matchesArticleWhere(a, args?.where)).length + ), + create: jest.fn( + async (args: { + data: Omit; + }) => { + const now = new Date(); + const rec: ArticleRecord = { + id: seq.article++, + title: args.data.title, + content: args.data.content ?? '', + userId: args.data.userId, + tags: Array.isArray(args.data.tags) ? args.data.tags : [], + images: Array.isArray(args.data.images) ? args.data.images : [], + createdAt: now, + updatedAt: now, + }; + db.articles.push(rec); + return { ...rec }; + } + ), + + update: jest.fn( + async (args: { + where: { id: number }; + data: Partial>; + }) => { + const target = db.articles.find((p) => p.id === args.where.id); + if (!target) { + const e = new Error('Not found') as Error & { code: string }; + e.code = 'P2025'; + throw e; + } + Object.assign(target, { + ...args.data, + tags: args.data.tags ?? target.tags, + images: args.data.images ?? target.images, + updatedAt: new Date(), + }); + return { ...target }; + } + ), + + delete: jest.fn(async (args: { where: { id: number } }) => { + const idx = db.articles.findIndex((a) => a.id === args.where.id); + if (idx === -1) { + const e = new Error('Not found') as Error & { code: string }; + e.code = 'P2025'; + throw e; + } + const [removed] = db.articles.splice(idx, 1); + return { ...removed }; + }), + + deleteMany: jest.fn(async (args?: { where?: { id?: number } }) => { + const before = db.articles.length; + if (!args?.where || args.where.id == null) { + db.articles = []; + } else { + const id = args.where.id; + db.articles = db.articles.filter((a) => a.id !== id); + } + const after = db.articles.length; + return { count: before - after }; + }), + }, + //#endregion + + //#region prisma.product + product: { + findMany: jest.fn(async (args?: FindManyProductArgs) => { + const list = db.products.filter((p) => + matchesProductWhere(p, args?.where) + ); + return list.map((p) => ensureProductArrays({ ...p })); + }), + + findUnique: jest.fn( + async ( + args: A + ): Promise> => { + const f = db.products.find((p) => p.id === args.where.id); + if (!f) return null as FindUniqueProductReturn; + + if (!('select' in args) || !args.select) { + return ensureProductArrays({ + ...f, + }) as unknown as FindUniqueProductReturn; + } + + const sel = args.select as ProductSelect; + const pickedEntries = Object.keys(sel) + .filter((k) => sel[k as keyof ProductSelect]) + .map( + (k) => + [k, f[k as keyof ProductRecord]] as [ + keyof ProductRecord | string, + unknown + ] + ); + const picked = Object.fromEntries(pickedEntries) as Pick< + ProductRecord, + Extract + >; + return picked as FindUniqueProductReturn; + } + ), + + count: jest.fn( + async (args?: { + where?: { + OR?: Array< + | { + name?: { + contains?: string; + mode?: InsensitiveMode; + }; + } + | { + description?: { + contains?: string; + mode?: InsensitiveMode; + }; + } + >; + }; + }) => + db.products.filter((p) => matchesProductWhere(p, args?.where)).length + ), + + create: jest.fn( + async (args: { + data: Omit; + }) => { + const now = new Date(); + const rec: ProductRecord = { + id: seq.product++, + name: args.data.name, + description: args.data.description, + price: args.data.price ?? 0, + userId: args.data.userId, + tags: Array.isArray(args.data.tags) ? args.data.tags : [], + images: Array.isArray(args.data.images) ? args.data.images : [], + createdAt: now, + updatedAt: now, + }; + db.products.push(rec); + return { ...rec }; + } + ), + + update: jest.fn( + async (args: { + where: { id: number }; + data: Partial>; + }) => { + const target = db.products.find((p) => p.id === args.where.id); + if (!target) { + const e = new Error('Not found') as Error & { code: string }; + e.code = 'P2025'; + throw e; + } + Object.assign(target, { + ...args.data, + tags: args.data.tags ?? target.tags, + images: args.data.images ?? target.images, + updatedAt: new Date(), + }); + return { ...target }; + } + ), + + delete: jest.fn(async (args: { where: { id: number } }) => { + const idx = db.products.findIndex((p) => p.id === args.where.id); + if (idx === -1) { + const e = new Error('Not found') as Error & { code: string }; + e.code = 'P2025'; + throw e; + } + const [removed] = db.products.splice(idx, 1); + return { ...removed }; + }), + + deleteMany: jest.fn(async (args?: { where?: { id?: number } }) => { + const before = db.products.length; + if (!args?.where || args.where.id == null) { + db.products = []; + } else { + const id = args.where.id; + db.products = db.products.filter((p) => p.id !== id); + } + const after = db.products.length; + return { count: before - after }; + }), + }, + //#endregion + + //#region prisma.comment + comment: { + findUnique: jest.fn(async (args: { where: { id: number } }) => { + const f = db.comments.find((c) => c.id === args.where.id); + return f ? { ...f } : null; + }), + + findMany: jest.fn( + async (args?: { + where?: { + id?: number | { in?: number[] }; + articleId?: number | null; + productId?: number | null; + userId?: number; + }; + orderBy?: { createdAt?: OrderDir }; + skip?: number; + take?: number; + }) => { + let list = db.comments.slice(); + + if (args?.where?.id !== undefined) { + const idCond = args.where.id as number | { in?: number[] }; + if (typeof idCond === 'number') { + list = list.filter((c) => c.id === idCond); + } else if (idCond && Array.isArray(idCond.in)) { + list = list.filter((c) => idCond.in!.includes(c.id)); + } + } + + if (args?.where?.articleId !== undefined) { + list = list.filter((c) => c.articleId === args.where!.articleId); + } + if (args?.where?.productId !== undefined) { + list = list.filter((c) => c.productId === args.where!.productId); + } + if (args?.where?.userId !== undefined) { + list = list.filter((c) => c.userId === args.where!.userId); + } + + if (args?.orderBy?.createdAt) { + const dir = args.orderBy.createdAt === 'asc' ? 1 : -1; + list.sort( + (a, b) => (a.createdAt.getTime() - b.createdAt.getTime()) * dir + ); + } + + const start = args?.skip ?? 0; + const end = args?.take != null ? start + args.take : undefined; + return list.slice(start, end).map((c) => ({ ...c })); + } + ), + + create: jest.fn( + async (args: { + data: Omit & + Partial>; + }) => { + const hasArticle = typeof args.data.articleId === 'number'; + const hasProduct = typeof args.data.productId === 'number'; + if (hasArticle === hasProduct) { + const e = new Error( + 'Exactly one of articleId/productId required' + ) as Error & { code: string }; + e.code = 'VALIDATION'; + throw e; + } + + const now = new Date(); + const rec: CommentRecord = { + id: args.data.id ?? seq.cmt++, + content: args.data.content ?? '', + userId: args.data.userId, + articleId: hasArticle ? args.data.articleId! : null, + productId: hasProduct ? args.data.productId! : null, + createdAt: now, + updatedAt: now, + }; + db.comments.push(rec); + return { ...rec }; + } + ), + + update: jest.fn( + async (args: { + where: { id: number }; + data: Partial>; + }) => { + const t = db.comments.find((c) => c.id === args.where.id); + if (!t) { + const e = new Error('Not found') as Error & { code: string }; + e.code = 'P2025'; + throw e; + } + Object.assign(t, { ...args.data, updatedAt: new Date() }); + return { ...t }; + } + ), + + delete: jest.fn(async (args: { where: { id: number } }) => { + const idx = db.comments.findIndex((c) => c.id === args.where.id); + if (idx < 0) { + const e = new Error('Not found') as Error & { code: string }; + e.code = 'P2025'; + throw e; + } + const [removed] = db.comments.splice(idx, 1); + return { ...removed }; + }), + }, + //#endregion + + //#region prisma.articleLike + articleLike: { + findUnique: jest.fn( + async ({ where }: { where: PairKey<'userId', 'articleId'> }) => { + const { userId, articleId } = pickArticleLikeKey(where); + return ( + db.articleLikes.find( + (l) => l.userId === userId && l.articleId === articleId + ) ?? null + ); + } + ), + + create: jest.fn( + async ({ data }: { data: { userId: number; articleId: number } }) => { + const userId = Number(data.userId); + const articleId = Number(data.articleId); + const exists = db.articleLikes.some( + (l) => l.userId === userId && l.articleId === articleId + ); + if (exists) { + const e = new Error('Unique constraint failed') as Error & { + code: string; + }; + e.code = 'P2002'; + throw e; + } + const row: ArticleLikeRecord = { id: seq.aLike++, userId, articleId }; + db.articleLikes.push(row); + return { ...row }; + } + ), + + delete: jest.fn( + async ({ where }: { where: PairKey<'userId', 'articleId'> }) => { + const { userId, articleId } = pickArticleLikeKey(where); + const idx = db.articleLikes.findIndex( + (l) => l.userId === userId && l.articleId === articleId + ); + if (idx < 0) { + const e = new Error('Not found') as Error & { code: string }; + e.code = 'P2025'; + throw e; + } + const [removed] = db.articleLikes.splice(idx, 1); + return { ...removed }; + } + ), + + count: jest.fn( + async (args: { where: ArticleLikeWhere }) => + db.articleLikes.filter((l) => matchArticleLike(args.where, l)).length + ), + + groupBy: jest.fn(async (args: ArticleLikeGroupByArgs) => { + const rawIds = args?.where?.articleId?.in ?? []; + const ids = isNumArray(rawIds) ? rawIds : []; + return ids.map((id) => { + const c = db.articleLikes.filter((l) => l.articleId === id).length; + return wantsCount(args?._count, 'articleId') + ? { articleId: id, _count: { articleId: c } } + : { articleId: id }; + }); + }), + }, + //#endregion + + //#region prisma.productLike + productLike: { + findUnique: jest.fn( + async ({ where }: { where: PairKey<'userId', 'productId'> }) => { + const { userId, productId } = pickProductLikeKey(where); + return ( + db.productLikes.find( + (l) => l.userId === userId && l.productId === productId + ) ?? null + ); + } + ), + + create: jest.fn( + async ({ data }: { data: { userId: number; productId: number } }) => { + const userId = Number(data.userId); + const productId = Number(data.productId); + const exists = db.productLikes.some( + (l) => l.userId === userId && l.productId === productId + ); + if (exists) { + const e = new Error('Unique constraint failed') as Error & { + code: string; + }; + e.code = 'P2002'; + throw e; + } + const row: ProductLikeRecord = { id: seq.pLike++, userId, productId }; + db.productLikes.push(row); + return { ...row }; + } + ), + + delete: jest.fn( + async ({ where }: { where: PairKey<'userId', 'productId'> }) => { + const { userId, productId } = pickProductLikeKey(where); + const idx = db.productLikes.findIndex( + (l) => l.userId === userId && l.productId === productId + ); + if (idx < 0) { + const e = new Error('Not found') as Error & { code: string }; + e.code = 'P2025'; + throw e; + } + const [removed] = db.productLikes.splice(idx, 1); + return { ...removed }; + } + ), + + count: jest.fn( + async (args: { where: ProductLikeWhere }) => + db.productLikes.filter((l) => matchProductLike(args.where, l)).length + ), + + groupBy: jest.fn(async (args: ProductLikeGroupByArgs) => { + const rawIds = args?.where?.productId?.in ?? []; + const ids = isNumArray(rawIds) ? rawIds : []; + return ids.map((id) => { + const c = db.productLikes.filter((l) => l.productId === id).length; + return wantsCount(args?._count, 'productId') + ? { productId: id, _count: { productId: c } } + : { productId: id }; + }); + }), + }, + //#endregion + + //#region prisma.commentLike + commentLike: { + findUnique: jest.fn( + async (args: { + where: { userId_commentId: { userId: number; commentId: number } }; + }) => { + const { userId, commentId } = args.where.userId_commentId; + return ( + db.commentLikes.find( + (l) => l.userId === userId && l.commentId === commentId + ) ?? null + ); + } + ), + + findMany: jest.fn(async (args?: FindManyCommentLikeArgs) => { + let list = db.commentLikes.slice(); + + if (args?.where) { + const w = args.where; + const hasCommentIdCond = + typeof w.commentId === 'number' || + (w.commentId && Array.isArray((w.commentId as { in?: number[] }).in)); + + if (hasCommentIdCond) { + let cond: CommentLikeWhere = + typeof w.commentId === 'number' + ? { commentId: w.commentId } + : { + commentId: { + in: (w.commentId as { in?: number[] }).in ?? [], + }, + }; + + if (typeof w.userId === 'number') { + cond = { ...cond, userId: w.userId }; + } + + list = list.filter((rec) => matchCommentLike(cond, rec)); + } + + if (typeof w.userId === 'number') { + list = list.filter((rec) => rec.userId === w.userId); + } + } + + if (args?.orderBy?.id) { + const dir = args.orderBy.id === 'asc' ? 1 : -1; + list.sort((a, b) => (a.id - b.id) * dir); + } + + const start = args?.skip ?? 0; + const end = args?.take != null ? start + args.take : undefined; + const sliced = list.slice(start, end); + + // ---------- include.comment 처리 ---------- + const commentArg = args?.include?.comment; + if (!commentArg) return sliced.map((x) => ({ ...x })); + + // 선택/포함 타입 정의 + type ArticleSelect = Partial>; + type ProductSelect = Partial>; + type CommentBaseSelect = Partial>; + + interface CommentSelectShape { + select: CommentBaseSelect & { + article?: { select?: ArticleSelect }; + product?: { select?: ProductSelect }; + }; + } + interface CommentIncludeShape { + include?: { + article?: { select?: ArticleSelect }; + product?: { select?: ProductSelect }; + }; + } + + // 타입 가드 + const hasSelect = (v: unknown): v is CommentSelectShape => + typeof v === 'object' && + v !== null && + 'select' in (v as Record); + + const hasInclude = (v: unknown): v is CommentIncludeShape => + typeof v === 'object' && + v !== null && + 'include' in (v as Record); + + const isSelect = hasSelect(commentArg); + const sel = isSelect ? commentArg.select : undefined; + + // 안전한 pick 유틸 + const pick = >( + obj: T | null, + keys?: Partial> + ): T | null => { + if (!obj) return null; + if (!keys) return { ...obj }; + const out = {} as T; + (Object.keys(keys) as Array).forEach((k) => { + if (keys[k]) { + out[k] = obj[k]; + } + }); + return out; + }; + + // include 모드에서 article/product 포함 여부 + const wantArticle = + !isSelect && hasInclude(commentArg) && !!commentArg.include?.article; + const wantProduct = + !isSelect && hasInclude(commentArg) && !!commentArg.include?.product; + + type CommentWithIncludes = CommentRecord & { + article?: ArticleRecord | null; + product?: ProductRecord | null; + }; + + return sliced.map((like) => { + const c = db.comments.find((x) => x.id === like.commentId) || null; + + // include 모드 (select 아님) + if (!isSelect) { + let comment: CommentWithIncludes | null = c ? { ...c } : null; + + if (comment && wantArticle) { + comment.article = + c!.articleId != null + ? db.articles.find((a) => a.id === c!.articleId) || null + : null; + } + if (comment && wantProduct) { + comment.product = + c!.productId != null + ? db.products.find((p) => p.id === c!.productId) || null + : null; + } + + return { ...like, comment }; + } + + // select 모드 + const commentSelected = pick(c, sel as CommentBaseSelect); + + // article select + if (sel?.article) { + const art = + c && c.articleId != null + ? db.articles.find((a) => a.id === c.articleId) || null + : null; + + (commentSelected as Record)['article'] = sel.article + .select + ? pick(art, sel.article.select) + : art; + } + + // product select + if (sel?.product) { + const prod = + c && c.productId != null + ? db.products.find((p) => p.id === c.productId) || null + : null; + + (commentSelected as Record)['product'] = sel.product + .select + ? pick(prod, sel.product.select) + : prod; + } + + return { ...like, comment: commentSelected }; + }); + }), + + create: jest.fn( + async (args: { data: { userId: number; commentId: number } }) => { + const { userId, commentId } = args.data; + const exists = db.commentLikes.some( + (l) => l.userId === userId && l.commentId === commentId + ); + if (exists) { + const e = new Error('Unique constraint failed') as Error & { + code: string; + }; + e.code = 'P2002'; + throw e; + } + const row: CommentLikeRecord = { id: seq.cLike++, userId, commentId }; + db.commentLikes.push(row); + return { ...row }; + } + ), + + delete: jest.fn( + async (args: { + where: { userId_commentId: { userId: number; commentId: number } }; + }) => { + const { userId, commentId } = args.where.userId_commentId; + const idx = db.commentLikes.findIndex( + (l) => l.userId === userId && l.commentId === commentId + ); + if (idx < 0) { + const e = new Error('Not found') as Error & { code: string }; + e.code = 'P2025'; + throw e; + } + const [removed] = db.commentLikes.splice(idx, 1); + return { ...removed }; + } + ), + + count: jest.fn( + async (args: { where: CommentLikeWhere }) => + db.commentLikes.filter((l) => matchCommentLike(args.where, l)).length + ), + + groupBy: jest.fn(async (args: CommentLikeGroupByArgs) => { + const rawIds = args?.where?.commentId?.in ?? []; + const ids = isNumArray(rawIds) ? rawIds : []; + return ids.map((id) => { + const c = db.commentLikes.filter((l) => l.commentId === id).length; + return wantsCount(args?._count, 'commentId') + ? { commentId: id, _count: { commentId: c } } + : { commentId: id }; + }); + }), + }, + //#endregion + + //#region prisma.notification + notification: { + findMany: jest.fn( + async (args?: { + where?: { userId?: number; isRead?: boolean }; + orderBy?: { createdAt?: OrderDir }; + skip?: number; + take?: number; + }) => { + let list = db.notifications.slice(); + + if (args?.where?.userId !== undefined) { + list = list.filter((n) => n.userId === args.where!.userId); + } + if (args?.where?.isRead !== undefined) { + list = list.filter((n) => n.isRead === args.where!.isRead); + } + + if (args?.orderBy?.createdAt) { + const dir = args.orderBy.createdAt === 'asc' ? 1 : -1; + list.sort( + (a, b) => (a.createdAt.getTime() - b.createdAt.getTime()) * dir + ); + } + + const start = args?.skip ?? 0; + const end = args?.take != null ? start + args.take : undefined; + return list.slice(start, end).map((n) => ({ ...n })); + } + ), + + count: jest.fn( + async (args?: { where?: { userId?: number; isRead?: boolean } }) => { + return db.notifications.filter((n) => { + if ( + args?.where?.userId !== undefined && + n.userId !== args.where!.userId + ) + return false; + if ( + args?.where?.isRead !== undefined && + n.isRead !== args.where!.isRead + ) + return false; + return true; + }).length; + } + ), + + updateMany: jest.fn( + async (args: { + where?: { userId?: number; isRead?: boolean }; + data: Partial>; + }) => { + let count = 0; + for (const n of db.notifications) { + const userOk = + args.where?.userId === undefined || n.userId === args.where!.userId; + const readOk = + args.where?.isRead === undefined || n.isRead === args.where!.isRead; + if (userOk && readOk) { + Object.assign(n, args.data, { updatedAt: new Date() }); + count++; + } + } + return { count }; + } + ), + + update: jest.fn( + async (args: { + where: { id: number }; + data: Partial>; + }) => { + const target = db.notifications.find((n) => n.id === args.where.id); + if (!target) { + const e = new Error('Not found') as Error & { code: string }; + e.code = 'P2025'; + throw e; + } + Object.assign(target, args.data, { updatedAt: new Date() }); + return { ...target }; + } + ), + + create: jest.fn( + async (args: { + data: Omit; + }) => { + const now = new Date(); + const rec: NotificationRecord = { + id: seq.notif++, + createdAt: now, + updatedAt: now, + ...args.data, + }; + db.notifications.push(rec); + return { ...rec }; + } + ), + + findUnique: jest.fn(async (args: { where: { id: number } }) => { + const n = db.notifications.find((x) => x.id === args.where.id); + return n ? { ...n } : null; + }), + }, + //#endregion +}; +//#endregion + +//#region Prisma Mock: $transaction & default export +type PrismaLike = typeof prisma & { + $transaction: jest.MockedFunction<(arg: unknown) => Promise>; +}; + +(prisma as PrismaLike).$transaction = jest.fn(async (arg: unknown) => { + if (Array.isArray(arg)) return Promise.all(arg as unknown[]); + if (typeof arg === 'function') + return (arg as (tx: typeof prisma) => unknown)(prisma); + return arg; +}); + +export default prisma; +//#endregion + +//#region Test Helpers (reset & seeders) +export function prismaReset(): void { + db.users = []; + db.articles = []; + db.products = []; + db.comments = []; + db.articleLikes = []; + db.productLikes = []; + db.commentLikes = []; + db.notifications = []; + seq = { + user: 1, + article: 1, + product: 1, + cmt: 1, + aLike: 1, + pLike: 1, + cLike: 1, + notif: 1, + }; + + prisma.user.create.mockClear(); + prisma.user.findUnique.mockClear(); + prisma.user.update.mockClear(); + + prisma.article.findMany.mockClear(); + prisma.article.findUnique.mockClear(); + prisma.article.count.mockClear(); + prisma.article.create.mockClear(); + prisma.article.update.mockClear(); + prisma.article.delete.mockClear(); + prisma.article.deleteMany.mockClear(); + + prisma.product.findMany.mockClear(); + prisma.product.findUnique.mockClear(); + prisma.product.count.mockClear(); + prisma.product.create.mockClear(); + prisma.product.update.mockClear(); + prisma.product.delete.mockClear(); + prisma.product.deleteMany.mockClear(); + + prisma.comment.findMany.mockClear(); + prisma.comment.findUnique.mockClear(); + prisma.comment.create.mockClear(); + prisma.comment.update.mockClear(); + prisma.comment.delete.mockClear(); + + prisma.articleLike.count.mockClear(); + prisma.articleLike.groupBy.mockClear(); + prisma.productLike.count.mockClear(); + prisma.productLike.groupBy.mockClear(); + prisma.commentLike.count.mockClear(); + prisma.commentLike.groupBy.mockClear(); + + prisma.notification.findMany.mockClear(); + prisma.notification.count.mockClear(); + prisma.notification.updateMany.mockClear(); + prisma.notification.update.mockClear(); + prisma.notification.create.mockClear(); + prisma.notification.findUnique.mockClear(); + + (prisma as PrismaLike).$transaction.mockClear(); +} + +export function seedUsers( + list: Array< + Pick & + Partial> + > +): void { + for (const u of list) { + db.users.push({ + id: u.id, + username: u.username, + email: u.email, + password: u.password, + images: u.images ?? [], + createdAt: u.createdAt ?? new Date(), + updatedAt: u.updatedAt ?? new Date(), + }); + } +} + +export async function seedUsersWithHash( + list: Array< + Pick & + Partial> + >, + opts: { saltRounds?: number } = {} +): Promise { + const saltRounds = opts.saltRounds ?? 10; + + for (const u of list) { + const hashed = await bcrypt.hash(u.password, saltRounds); + db.users.push({ + id: u.id, + username: u.username, + email: u.email, + password: hashed, + images: u.images ?? [], + createdAt: u.createdAt ?? new Date(), + updatedAt: u.updatedAt ?? new Date(), + }); + } +} + +export function seedArticles( + list: Array< + Pick & Partial + > +): void { + for (const a of list) { + db.articles.push({ + id: a.id, + title: a.title, + content: a.content ?? '', + userId: a.userId, + tags: a.tags ?? [], + images: a.images ?? [], + createdAt: a.createdAt ?? new Date(), + updatedAt: a.updatedAt ?? new Date(), + }); + } +} + +export function seedProducts( + list: Array< + Pick & + Partial + > +): void { + for (const p of list) { + db.products.push({ + id: p.id, + name: p.name, + description: p.description ?? '', + price: p.price, + userId: p.userId, + tags: p.tags ?? [], + images: p.images ?? [], + createdAt: p.createdAt ?? new Date(), + updatedAt: p.updatedAt ?? new Date(), + }); + } +} + +export function seedComments( + list: Array< + Pick & + ( + | { articleId: number; productId?: undefined } + | { productId: number; articleId?: undefined } + ) & + Partial> + > +): CommentRecord[] { + const out: CommentRecord[] = []; + for (const c of list) { + const hasArticle = + typeof (c as { articleId?: number }).articleId === 'number'; + const hasProduct = + typeof (c as { productId?: number }).productId === 'number'; + if (hasArticle === hasProduct) { + throw new Error( + 'seedComments: require exactly one of articleId or productId' + ); + } + const rec: CommentRecord = { + id: c.id, + content: c.content, + userId: c.userId, + articleId: (c as { articleId?: number }).articleId ?? null, + productId: (c as { productId?: number }).productId ?? null, + createdAt: c.createdAt ?? new Date(), + updatedAt: c.updatedAt ?? new Date(), + }; + db.comments.push(rec); + out.push(rec); + } + return out; +} + +export function seedArticleLikes( + pairs: ReadonlyArray> +): void { + for (const x of pairs) db.articleLikes.push({ id: seq.aLike++, ...x }); +} + +export function seedProductLikes( + pairs: ReadonlyArray> +): void { + for (const x of pairs) db.productLikes.push({ id: seq.pLike++, ...x }); +} + +export function seedCommentLikes( + list: Array> +): void { + for (const x of list) db.commentLikes.push({ id: seq.cLike++, ...x }); +} + +export function seedNotifications( + list: Array< + Omit & + Partial> + > +): void { + for (const n of list) { + const now = new Date(); + db.notifications.push({ + id: n.id ?? seq.notif++, + userId: n.userId, + type: n.type, + message: n.message, + isRead: n.isRead ?? false, + createdAt: n.createdAt ?? now, + updatedAt: n.updatedAt ?? now, + }); + } +} +//#endregion diff --git a/part4-mission11/tests/_helper/test-app.ts b/part4-mission11/tests/_helper/test-app.ts new file mode 100644 index 000000000..1b8e48496 --- /dev/null +++ b/part4-mission11/tests/_helper/test-app.ts @@ -0,0 +1,5 @@ +export async function createTestApp() { + const mod = await import('../../src/app.js'); + const app = await mod.buildApp({ forTest: true }); + return app as import('express').Express; +} diff --git a/part4-mission11/tests/_helper/test-passport-app.ts b/part4-mission11/tests/_helper/test-passport-app.ts new file mode 100644 index 000000000..a6b4c50fe --- /dev/null +++ b/part4-mission11/tests/_helper/test-passport-app.ts @@ -0,0 +1,36 @@ +import express, { type ErrorRequestHandler } from 'express'; +import passport from 'passport'; + +import { localStrategy } from '../../src/lib/passport/localStrategy.js'; + +type MaybeHttpError = { + status?: unknown; + message?: unknown; +}; + +export function createPassportTestApp() { + const app = express(); + app.use(express.json()); + passport.use(localStrategy); + app.use(passport.initialize()); + + app.post( + '/login', + passport.authenticate('local', { session: false }), + (req, res) => { + res.status(200).json({ ok: true, user: req.user }); + } + ); + + // 에러 바디 노출 (디버깅용) + const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => { + const e = err as MaybeHttpError; + const status = typeof e.status === 'number' ? e.status : 500; + const message = + typeof e.message === 'string' ? e.message : 'Internal Server Error'; + res.status(status).json({ message }); + }; + app.use(errorHandler); + + return app; +} diff --git a/part4-mission11/tests/_helper/test-utils.ts b/part4-mission11/tests/_helper/test-utils.ts new file mode 100644 index 000000000..489b595d0 --- /dev/null +++ b/part4-mission11/tests/_helper/test-utils.ts @@ -0,0 +1,63 @@ +import './mock-modules.js'; +import bcrypt from 'bcrypt'; +import request from 'supertest'; + +import { prisma } from './prisma-mock.js'; + +export async function getPrismaMock() { + return prisma; +} + +export async function loginAndGetSession( + app: import('express').Express, + opts?: { + loginPath?: string; + username?: string; + password?: string; + userId?: number; + seedDb?: boolean; + resetBeforeSeed?: boolean; + } +) { + const { + loginPath = '/users/login', + username = 'u', + password = 'pw', + userId = 7, + seedDb = false, + resetBeforeSeed = false, + } = opts ?? {}; + + if (seedDb) { + const { prismaReset, seedUsersWithHash } = await import('./prisma-mock.js'); + if (resetBeforeSeed) prismaReset(); + await seedUsersWithHash([ + { id: userId, username, email: 'u@ex.com', password }, + ]); + } + + const prisma = await getPrismaMock(); + + prisma.user.findUnique.mockResolvedValue({ + id: userId, + username, + email: 'u@ex.com', + password: await bcrypt.hash(password, 10), + createdAt: new Date(), + updatedAt: new Date(), + images: [], + }); + + const res = await request(app) + .post(loginPath) + .send({ username, password }) + .expect(200); + + const setCookies = res.headers['set-cookie'] ?? []; + const cookiePairs = ( + Array.isArray(setCookies) ? setCookies : [setCookies] + ).map((c) => c.split(';')[0]); + const accessToken = res.body?.accessToken as string | undefined; + + return { cookies: cookiePairs, accessToken, user: { id: userId, username } }; +} diff --git a/part4-mission11/tests/_helper/token-mock.ts b/part4-mission11/tests/_helper/token-mock.ts new file mode 100644 index 000000000..b8305ebe2 --- /dev/null +++ b/part4-mission11/tests/_helper/token-mock.ts @@ -0,0 +1,15 @@ +import { jest } from '@jest/globals'; + +export type Tokens = { accessToken: string; refreshToken: string }; + +export const generateTokens = jest.fn<(userId: number) => Tokens>(() => ({ + accessToken: 'A.T', + refreshToken: 'R.T', +})); + +export const verifyRefreshToken = jest.fn< + (token: string) => { userId: number } +>(() => ({ userId: 7 })); + +// default 있어도 되지만, 테스트에서는 named export로 쓰는 걸 권장 +export default { generateTokens, verifyRefreshToken }; diff --git a/part4-mission11/tests/_helper/ws-test-util.ts b/part4-mission11/tests/_helper/ws-test-util.ts new file mode 100644 index 000000000..e5d0ed967 --- /dev/null +++ b/part4-mission11/tests/_helper/ws-test-util.ts @@ -0,0 +1,41 @@ +import type { Socket } from 'socket.io-client'; + +export function waitConnect(s: Socket) { + return new Promise((res, rej) => { + const ok = () => { + cleanup(); + res(); + }; + const er = (e: unknown) => { + cleanup(); + rej(e); + }; + const cleanup = () => { + s.off('connect', ok); + s.off('connect_error', er as (e: Error) => void); + }; + s.once('connect', ok); + s.once('connect_error', er as (e: Error) => void); + }); +} + +export function waitDisconnect(s: Socket) { + return new Promise((res) => { + const done = () => { + s.off('disconnect', done); + res(); + }; + s.once('disconnect', done); + }); +} + +export async function cleanClient(s?: Socket) { + if (!s) return; + try { + s.removeAllListeners(); + s.disconnect(); // alias: close() + await waitDisconnect(s); + } catch { + // ignore + } +} diff --git a/part4-mission11/tests/int/int.articles.public.test.ts b/part4-mission11/tests/int/int.articles.public.test.ts new file mode 100644 index 000000000..330d4d4d1 --- /dev/null +++ b/part4-mission11/tests/int/int.articles.public.test.ts @@ -0,0 +1,68 @@ +import request from 'supertest'; + +import { + prismaReset, + seedArticles, + seedArticleLikes, + seedCommentLikes, +} from '../_helper/prisma-mock.js'; +import { createTestApp } from '../_helper/test-app.js'; + +describe('[통합] 게시글 API (비인증)', () => { + let app: import('express').Express; + + beforeAll(async () => { + app = await createTestApp(); + }); + + beforeEach(() => { + prismaReset(); + seedArticles([ + { id: 21, title: 'B', userId: 101, images: ['b1.png'], tags: [] }, + { id: 22, title: 'C', userId: 101, images: ['c1.png'], tags: [] }, + ]); + seedArticleLikes([ + { articleId: 21, userId: 1 }, + { articleId: 21, userId: 2 }, + ]); + seedCommentLikes([{ commentId: 100, userId: 1 }]); + }); + + test('GET /articles?query=__not_exists__ → 200 []', async () => { + const app = await createTestApp(); + seedArticles([{ id: 1, title: 'Foo', userId: 1 }]); + const res = await request(app) + .get('/articles?query=__not_exists__') + .expect(200); + expect(res.body.data.length).toBe(0); + }); + + test('GET /articles → 200 + 목록', async () => { + const res = await request(app) + .get('/articles') + .query({ page: 1, pageSize: 10 }) + .expect(200); + + expect(res.body).toEqual( + expect.objectContaining({ + data: expect.arrayContaining([ + expect.objectContaining({ id: 21, title: 'B' }), + expect.objectContaining({ id: 22, title: 'C' }), + ]), + }) + ); + }); + + test('GET /articles/:id → 200 + 단건', async () => { + const res = await request(app).get('/articles/21').expect(200); + expect(res.body).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ id: 21, title: 'B' }), + }) + ); + }); + + test('GET /articles/404 → 404', async () => { + await request(app).get('/articles/404').expect(404); + }); +}); diff --git a/part4-mission11/tests/int/int.articles.secure.test.ts b/part4-mission11/tests/int/int.articles.secure.test.ts new file mode 100644 index 000000000..73fb5fcb2 --- /dev/null +++ b/part4-mission11/tests/int/int.articles.secure.test.ts @@ -0,0 +1,146 @@ +import '../_helper/mock-modules.js'; +import request from 'supertest'; + +import { prismaReset, seedArticles } from '../_helper/prisma-mock.js'; +import { createTestApp } from '../_helper/test-app.js'; +import { loginAndGetSession } from '../_helper/test-utils.js'; + +describe('[통합] 게시글 API (인증 필요)', () => { + let app: import('express').Express; + + beforeAll(async () => { + app = await createTestApp(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + prismaReset(); + }); + + test('POST /articles → 201 (쿠키 필요)', async () => { + const { accessToken } = await loginAndGetSession(app, { userId: 7 }); + + const res = await request(app) + .post('/articles') + .set('Authorization', `Bearer ${accessToken}`) + .send({ title: 'N', content: 'd', tags: [], images: [] }) + .expect(201); + + const createdId = res.body?.data?.id ?? 2; + const getRes = await request(app).get(`/articles/${createdId}`).expect(200); + expect(getRes.body.data).toEqual( + expect.objectContaining({ title: 'N', content: 'd' }) + ); + }); + + test('POST /articles → 401 (토큰 없음)', async () => { + await request(app) + .post('/articles') + .send({ title: 'N', content: 'd', tags: [], images: [] }) + .expect(401); + }); + + test('POST /articles → 400 (검증 실패: 빈 title)', async () => { + const { accessToken } = await loginAndGetSession(app); + const res = await request(app) + .post('/articles') + .set('Authorization', `Bearer ${accessToken}`) + .send({ title: '', content: 'd', tags: [], images: [] }) + .expect(400); + + expect(res.body).toEqual( + expect.objectContaining({ + message: expect.stringMatching('제목은 필수입니다.'), + }) + ); + }); + + test('PATCH /articles/:id → 200 (쿠키 필요)', async () => { + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + seedArticles([{ id: 1, title: 'N', userId: user.id }]); + await request(app) + .patch('/articles/1') + .set('Authorization', `Bearer ${accessToken}`) + .send({ title: 'N2', content: 'd2', tags: [], images: [] }) + .expect(200); + + const res = await request(app).get('/articles/1').expect(200); + expect(res.body.data).toEqual( + expect.objectContaining({ id: 1, title: 'N2', content: 'd2' }) + ); + }); + + test('PATCH /articles/:id → 403 (소유자 아님)', async () => { + prismaReset(); + seedArticles([{ id: 2, title: 'X', userId: 999 }]); + const { accessToken } = await loginAndGetSession(app, { userId: 7 }); + + const res = await request(app) + .patch('/articles/2') + .set('Authorization', `Bearer ${accessToken}`) + .send({ title: 'X2', content: 'd2', tags: [], images: [] }) + .expect(403); + + expect(res.body).toEqual( + expect.objectContaining({ + message: expect.stringMatching('권한이 없습니다.'), + }) + ); + }); + + test('DELETE /articles/:id → 200 (쿠키 필요)', async () => { + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + seedArticles([{ id: 1, title: 'N', userId: user.id }]); + + await request(app) + .delete('/articles/1') + .set('Authorization', `Bearer ${accessToken}`) + .expect(204); + }); + + test('DELETE /articles/:id → 204 & 이후 조회 404', async () => { + prismaReset(); + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + seedArticles([{ id: 3, title: 'T', userId: user.id }]); + await request(app) + .delete('/articles/3') + .set('Authorization', `Bearer ${accessToken}`) + .expect(204); + + await request(app).get('/articles/3').expect(404); + }); + + test('POST/DELETE /articles/:id/like → 200 → 204', async () => { + prismaReset(); + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + seedArticles([{ id: 10, title: 'L', userId: user.id }]); + + const likeRes = await request(app) + .post('/articles/10/like') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + expect(likeRes.body).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ + message: expect.stringMatching(/완료|liked/i), + likeCount: expect.any(Number), + }), + }) + ); + + const unlikeRes = await request(app) + .delete('/articles/10/like') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + expect(unlikeRes.body).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ + message: expect.stringMatching(/취소|unliked/i), + likeCount: expect.any(Number), + }), + }) + ); + }); +}); diff --git a/part4-mission11/tests/int/int.auth.test.ts b/part4-mission11/tests/int/int.auth.test.ts new file mode 100644 index 000000000..b1e7b90c7 --- /dev/null +++ b/part4-mission11/tests/int/int.auth.test.ts @@ -0,0 +1,370 @@ +import { + afterEach, + beforeAll, + beforeEach, + describe, + jest, +} from '@jest/globals'; +import bcrypt from 'bcrypt'; +import type { JwtPayload } from 'jsonwebtoken'; +import request from 'supertest'; +import type { Response as SupertestResponse } from 'supertest'; + +import { + ACCESS_TOKEN_COOKIE_NAME, + REFRESH_TOKEN_COOKIE_NAME, +} from '../../src/lib/constants.js'; +import { prisma } from '../../src/lib/prismaClient.js'; +import { validation } from '../../src/middlewares/validation.js'; +import { asMockFn, type Awaited } from '../_helper/jest-typed.js'; + +export function extractCookieUnsafe( + res: SupertestResponse, + name: string +): string | null { + const raw = res.get?.('Set-Cookie') as string | string[] | undefined; + const raw2 = + raw ?? + (res.headers?.['set-cookie'] as unknown as string | string[] | undefined); + + const arr: string[] = Array.isArray(raw2) ? raw2 : raw2 ? [raw2] : []; + const item = arr.find( + (c) => typeof c === 'string' && c.startsWith(`${name}=`) + ); + if (!item) return null; + + const semi = item.indexOf(';'); + const first = semi >= 0 ? item.slice(0, semi) : item; + const eq = first.indexOf('='); + return eq >= 0 ? first.slice(eq + 1) : null; +} + +describe('[통합] 인증 (회원가입/로그인)', () => { + let app: import('express').Express; + + beforeAll(async () => { + jest.spyOn(validation, 'validateRegister').mockImplementation((( + req, + _res, + next + ) => { + if ( + typeof req.body?.email !== 'string' || + typeof req.body?.username !== 'string' || + typeof req.body?.password !== 'string' + ) { + } + next(); + return Promise.resolve(); + }) as typeof validation.validateRegister); + + const { createTestApp } = await import('../_helper/test-app.js'); + app = await createTestApp(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(console, 'log').mockImplementation(() => {}); + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + (console.log as unknown as jest.Mock).mockRestore(); + (console.error as unknown as jest.Mock).mockRestore(); + }); + + // --------------------------- + // 회원가입 + // --------------------------- + test('POST /users/register → 201 + 생성된 유저 일부 필드', async () => { + const now = new Date(); + + type UserCreateArgs = Parameters[0]; + type UserEntity = Awaited>; + + asMockFn(prisma.user.create).mockResolvedValue({ + id: 101, + email: 'new@ex.com', + username: 'newbie', + password: await bcrypt.hash('1234abcd!', 10), + images: [] as string[], + createdAt: now, + updatedAt: now, + } satisfies UserEntity); + + const res = await request(app) + .post('/users/register') + .send({ email: 'new@ex.com', username: 'newbie', password: '1234abcd!' }) + .expect(201); + + const body = res.body ?? {}; + const flattened = body.data?.user ?? body.data ?? body.user ?? body; + + expect(flattened).toEqual( + expect.objectContaining({ + id: expect.any(Number), + email: 'new@ex.com', + username: 'newbie', + }) + ); + + expect(prisma.user.create).toHaveBeenCalledTimes(1); + }); + + test('POST /users/register (중복 이메일) → 409', async () => { + const p2002 = Object.assign(new Error('P2002'), { + code: 'P2002' as const, + meta: { target: ['email'] as string[] }, + }) as Error & { code: 'P2002'; meta: { target: string[] } }; + + type UserCreateArgs = Parameters[0]; + type UserEntity = Awaited>; + + asMockFn( + prisma.user.create + ).mockRejectedValueOnce(p2002); + + const res = await request(app) + .post('/users/register') + .send({ email: 'dup@ex.com', username: 'dup', password: '1234abcd!' }) + .expect(409); + + expect(res.body).toEqual( + expect.objectContaining({ + status: 409, + message: expect.stringMatching(/이미 사용 중인 이메일|중복/), + }) + ); + }); + + // --------------------------- + // 로그인 + // --------------------------- + test('POST /users/login → 200 + 토큰 + 쿠키', async () => { + const hashed = await bcrypt.hash('1234abcd!', 10); + + type FindUniqueArgs = Parameters[0]; + type FindUniqueRet = Awaited>; + + asMockFn( + prisma.user.findUnique + ).mockResolvedValue({ + id: 7, + username: 'u', + email: 'u@ex.com', + password: hashed, + images: [] as string[], + createdAt: new Date(), + updatedAt: new Date(), + } satisfies NonNullable); + + const res = await request(app) + .post('/users/login') + .send({ username: 'u', password: '1234abcd!' }) + .expect(200); + + expect(res.body).toEqual( + expect.objectContaining({ + accessToken: expect.any(String), + refreshToken: expect.any(String), + }) + ); + + const rawCookies = res.get('Set-Cookie') ?? []; + const cookies = ( + Array.isArray(rawCookies) ? rawCookies : [rawCookies] + ).join(';'); + + expect(typeof ACCESS_TOKEN_COOKIE_NAME).toBe('string'); + expect(typeof REFRESH_TOKEN_COOKIE_NAME).toBe('string'); + + expect(cookies).toMatch( + new RegExp( + `${ACCESS_TOKEN_COOKIE_NAME.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')}=` + ) + ); + expect(cookies).toMatch( + new RegExp( + `${REFRESH_TOKEN_COOKIE_NAME.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')}=` + ) + ); + }); + + test('POST /users/login (없는 유저) → 400', async () => { + type FindUniqueArgs = Parameters[0]; + type FindUniqueRet = Awaited>; + + asMockFn( + prisma.user.findUnique + ).mockResolvedValueOnce(null); + + await request(app) + .post('/users/login') + .send({ username: 'nope', password: 'whatever' }) + .expect(401); + }); + + test('POST /users/login (비밀번호 불일치) → 400', async () => { + const hashedReal = await bcrypt.hash('real-pass', 10); + + type FindUniqueArgs = Parameters[0]; + type FindUniqueRet = Awaited>; + + asMockFn( + prisma.user.findUnique + ).mockResolvedValueOnce({ + id: 9, + username: 'u2', + email: 'u2@ex.com', + password: hashedReal, + images: [] as string[], + createdAt: new Date(), + updatedAt: new Date(), + } satisfies NonNullable); + + await request(app) + .post('/users/login') + .send({ username: 'u2', password: 'wrong-pass' }) + .expect(401); + }); + + test('POST /auth/refresh → 200 + accessToken 재발급 & refresh 회전', async () => { + const hashed = await bcrypt.hash('1234abcd!', 10); + + type FindUniqueArgs = Parameters[0]; + type FindUniqueRet = Awaited>; + + asMockFn( + prisma.user.findUnique + ).mockResolvedValue({ + id: 77, + username: 'ruser', + email: 'r@ex.com', + password: hashed, + images: [], + createdAt: new Date(), + updatedAt: new Date(), + } satisfies NonNullable); + + const loginRes = await request(app) + .post('/users/login') + .send({ username: 'ruser', password: '1234abcd!' }) + .expect(200); + + const setCookie = loginRes.get('Set-Cookie') ?? []; + const cookieHeader = (Array.isArray(setCookie) ? setCookie : [setCookie]) + .map((c) => c.split(';')[0]) + .join('; '); + + const res = await request(app) + .post('/auth/refresh') + .set('Cookie', cookieHeader) + .expect(200); + + expect(res.body).toEqual( + expect.objectContaining({ accessToken: expect.any(String) }) + ); + + const refreshedCookies = (res.get('Set-Cookie') ?? []).join(';'); + expect(refreshedCookies).toMatch( + new RegExp( + `${ACCESS_TOKEN_COOKIE_NAME.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')}=` + ) + ); + expect(refreshedCookies).toMatch( + new RegExp( + `${REFRESH_TOKEN_COOKIE_NAME.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')}=` + ) + ); + }); + + test('POST /auth/refresh (쿠키 없음) → 401', async () => { + const res = await request(app).post('/auth/refresh').expect(401); + expect(res.body).toEqual( + expect.objectContaining({ message: expect.any(String) }) + ); + }); + + test('POST /auth/refresh (토큰 위조: 재서명으로 시그니처 불일치) → 401', async () => { + type FindUniqueArgs = Parameters[0]; + type FindUniqueRet = Awaited>; + + const hashed = await bcrypt.hash('1234abcd!', 10); + asMockFn( + prisma.user.findUnique + ).mockResolvedValue({ + id: 777, + username: 'siguser', + email: 'sig@ex.com', + password: hashed, + images: [], + createdAt: new Date(), + updatedAt: new Date(), + } satisfies NonNullable); + + const loginRes = await request(app) + .post('/users/login') + .send({ username: 'siguser', password: '1234abcd!' }) + .expect(200); + + const refreshRaw = extractCookieUnsafe(loginRes, REFRESH_TOKEN_COOKIE_NAME); + expect(typeof refreshRaw).toBe('string'); + + const jwt = (await import('jsonwebtoken')).default; + const C1 = await import('../../src/lib/constants.js'); + + const decoded = jwt.decode(refreshRaw!) as JwtPayload | null; + const sub = (decoded?.sub as unknown as number) ?? 777; + + const BAD_SECRET = `${C1.REFRESH_TOKEN_SECRET}__FORGED__`; + const forged = jwt.sign({ sub, type: 'refresh' }, BAD_SECRET, { + algorithm: 'HS256', + expiresIn: '7d', + }); + + expect(() => + jwt.verify(forged, C1.REFRESH_TOKEN_SECRET, { algorithms: ['HS256'] }) + ).toThrow(); + + jest.resetModules(); + const { verifyRefreshToken } = await import('../../src/lib/token.js'); + + expect(() => verifyRefreshToken(forged)).toThrow( + /유효하지 않은 리프레시 토큰/ + ); + + const fakeCookieHeader = `${REFRESH_TOKEN_COOKIE_NAME}=${encodeURIComponent( + forged + )}`; + await request(app) + .post('/auth/refresh') + .set('Cookie', [fakeCookieHeader]) + .expect(401); + }); + + // 401: Authorization 헤더 없음 + test('GET /users/me → 401 (헤더 없음)', async () => { + await request(app).get('/users/me').expect(401); + }); + + // 401: Bearer 형식 깨짐 + test('GET /users/me → 401 (잘못된 Bearer 형식)', async () => { + await request(app) + .get('/users/me') + .set('Authorization', 'Token abc') + .expect(401); + }); + + // 401: refresh 쿠키 없음 + test('POST /auth/refresh → 401 (쿠키 없음)', async () => { + await request(app).post('/auth/refresh').expect(401); + }); + + // 401: 위조/쓰레기 쿠키 + test('POST /auth/refresh → 401 (위조 쿠키)', async () => { + await request(app) + .post('/auth/refresh') + .set('Cookie', [`REFRESH_TOKEN=fake.jwt.parts`]) + .expect(401); + }); +}); diff --git a/part4-mission11/tests/int/int.comments.secure.test.ts b/part4-mission11/tests/int/int.comments.secure.test.ts new file mode 100644 index 000000000..41ea08a2d --- /dev/null +++ b/part4-mission11/tests/int/int.comments.secure.test.ts @@ -0,0 +1,269 @@ +import request from 'supertest'; + +import { + prismaReset, + seedArticles, + seedProducts, + seedComments, +} from '../_helper/prisma-mock.js'; +import { createTestApp } from '../_helper/test-app.js'; +import { loginAndGetSession } from '../_helper/test-utils.js'; + +/* ----------------------------- + * 타입: API 응답 & DTO + * --------------------------- */ +type CommentDTO = { + id: number; + content: string; + // 필요시 확장: userId, articleId/productId 등 +}; + +type ApiData = { data: T }; +type ApiList = { data: T[] }; + +const asBody = (res: request.Response): T => res.body as unknown as T; + +describe('[통합] 게시글 댓글 API (인증 필요)', () => { + let app: import('express').Express; + + beforeAll(async () => { + app = await createTestApp(); + }); + beforeEach(() => { + prismaReset(); + }); + + test('GET /articles/:articleId/comments → 200 (토큰 필요) + 목록', async () => { + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + seedArticles([{ id: 1, title: 'A', userId: user.id }]); + seedComments([{ id: 101, articleId: 1, userId: user.id, content: 'c' }]); + + const res = await request(app) + .get('/articles/1/comments') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + const body = asBody>(res); + expect(Array.isArray(body.data)).toBe(true); + expect(body.data[0]).toEqual( + expect.objectContaining({ + id: expect.any(Number), + content: expect.any(String), + }) + ); + }); + + test('GET /articles/:articleId/comments → 200 (토큰 없음, 비로그인)', async () => { + seedArticles([{ id: 1, title: 'A', userId: 7 }]); + await request(app).get('/articles/1/comments').expect(200); + }); + + test('POST /articles/:articleId/comments → 201 (토큰 필요)', async () => { + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + seedArticles([{ id: 1, title: 'A', userId: user.id }]); + + const res = await request(app) + .post('/articles/1/comments') + .set('Authorization', `Bearer ${accessToken}`) + .send({ content: 'hello' }) + .expect(201); + + const body = asBody>(res); + expect(body.data).toEqual( + expect.objectContaining({ + id: expect.any(Number), + content: 'hello', + }) + ); + }); + + test('POST /articles/:id/comments → 401 (토큰 없음)', async () => { + const app = await createTestApp(); + seedArticles([{ id: 1, title: 'A', userId: 7 }]); + await request(app) + .post('/articles/1/comments') + .send({ content: 'x' }) + .expect(401); + }); + + test('PATCH /comments/:commentId → 200 (소유자) / 403 (비소유자)', async () => { + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + seedArticles([{ id: 1, title: 'A', userId: user.id }]); + const [c] = seedComments([ + { id: 201, articleId: 1, userId: user.id, content: 'old' }, + ]); + if (!c) throw new Error('seedComments failed'); + const cId = c.id; + + await request(app) + .patch(`/articles/comments/${cId}`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ content: 'new' }) + .expect(200); + + const { accessToken: other } = await loginAndGetSession(app, { + userId: 999, + }); + await request(app) + .patch(`/articles/comments/${cId}`) + .set('Authorization', `Bearer ${other}`) + .send({ content: 'hack' }) + .expect(403); + }); + + test('POST/DELETE /comments/:id/like → 200 → (200 or 204)', async () => { + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + seedArticles([{ id: 1, title: 'A', userId: user.id }]); + const [c] = seedComments([ + { id: 301, articleId: 1, userId: user.id, content: 'c' }, + ]); + if (!c) throw new Error('seedComments failed'); + const cId = c.id; + + const likeRes = await request(app) + .post(`/articles/comments/${cId}/like`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + const likeBody = + asBody>(likeRes); + expect(likeBody.data.message).toEqual(expect.stringMatching(/완료|liked/i)); + expect(typeof likeBody.data.likeCount).toBe('number'); + + const unlikeRes = await request(app) + .delete(`/articles/comments/${cId}/like`) + .set('Authorization', `Bearer ${accessToken}`) + .expect((res) => { + if (![200, 204].includes(res.status)) { + throw new Error(`expected 200 or 204, got ${res.status}`); + } + }); + + if (unlikeRes.status === 200) { + const unlikeBody = + asBody>(unlikeRes); + expect(unlikeBody.data.message).toEqual( + expect.stringMatching(/취소|unliked/i) + ); + expect(typeof unlikeBody.data.likeCount).toBe('number'); + } + }); +}); + +describe('[통합] 상품 댓글 API (인증 필요)', () => { + let app: import('express').Express; + + beforeAll(async () => { + app = await createTestApp(); + }); + beforeEach(() => { + prismaReset(); + }); + + test('GET /products/:productId/comments → 200 (토큰 필요) + 목록', async () => { + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + seedProducts([{ id: 1, name: 'A', price: 111, userId: user.id }]); + seedComments([{ id: 101, productId: 1, userId: user.id, content: 'c' }]); + + const res = await request(app) + .get('/products/1/comments') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + const body = asBody>(res); + expect(Array.isArray(body.data)).toBe(true); + expect(body.data[0]).toEqual( + expect.objectContaining({ + id: expect.any(Number), + content: expect.any(String), + }) + ); + }); + + test('GET /products/:productId/comments → 200 (토큰 없음, 비로그인)', async () => { + seedProducts([{ id: 1, name: 'A', price: 111, userId: 7 }]); + await request(app).get('/products/1/comments').expect(200); + }); + + test('POST /products/:productId/comments → 201 (토큰 필요)', async () => { + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + seedProducts([{ id: 1, name: 'A', price: 111, userId: user.id }]); + + const res = await request(app) + .post('/products/1/comments') + .set('Authorization', `Bearer ${accessToken}`) + .send({ content: 'hello' }) + .expect(201); + + const body = asBody>(res); + expect(body.data).toEqual( + expect.objectContaining({ + id: expect.any(Number), + content: 'hello', + }) + ); + }); + + test('PATCH /comments/:commentId → 200 (소유자) / 403 (비소유자)', async () => { + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + seedProducts([{ id: 1, name: 'A', price: 111, userId: user.id }]); + const [c] = seedComments([ + { id: 201, productId: 1, userId: user.id, content: 'old' }, + ]); + if (!c) throw new Error('seedComments failed'); + const cId = c.id; + + await request(app) + .patch(`/products/comments/${cId}`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ content: 'new' }) + .expect(200); + + const { accessToken: other } = await loginAndGetSession(app, { + userId: 999, + }); + await request(app) + .patch(`/products/comments/${cId}`) + .set('Authorization', `Bearer ${other}`) + .send({ content: 'hack' }) + .expect(403); + }); + + test('POST/DELETE /comments/:id/like → 200 → (200 or 204)', async () => { + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + seedProducts([{ id: 1, name: 'A', price: 111, userId: user.id }]); + const [c] = seedComments([ + { id: 301, productId: 1, userId: user.id, content: 'c' }, + ]); + if (!c) throw new Error('seedComments failed'); + const cId = c.id; + + const likeRes = await request(app) + .post(`/products/comments/${cId}/like`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + const likeBody = + asBody>(likeRes); + expect(likeBody.data.message).toEqual(expect.stringMatching(/완료|liked/i)); + expect(typeof likeBody.data.likeCount).toBe('number'); + + const unlikeRes = await request(app) + .delete(`/products/comments/${cId}/like`) + .set('Authorization', `Bearer ${accessToken}`) + .expect((res) => { + if (![200, 204].includes(res.status)) { + throw new Error(`expected 200 or 204, got ${res.status}`); + } + }); + + if (unlikeRes.status === 200) { + const unlikeBody = + asBody>(unlikeRes); + expect(unlikeBody.data.message).toEqual( + expect.stringMatching(/취소|unliked/i) + ); + expect(typeof unlikeBody.data.likeCount).toBe('number'); + } + }); +}); diff --git a/part4-mission11/tests/int/int.notifications.test.ts b/part4-mission11/tests/int/int.notifications.test.ts new file mode 100644 index 000000000..f192d875e --- /dev/null +++ b/part4-mission11/tests/int/int.notifications.test.ts @@ -0,0 +1,107 @@ +import bcrypt from 'bcrypt'; +import request from 'supertest'; + +import { prisma } from '../../src/lib/prismaClient.js'; +import { createTestApp } from '../_helper/test-app.js'; +import { loginAndGetSession } from '../_helper/test-utils.js'; + +jest.setTimeout(20_000); + +describe('Notifications API (secure)', () => { + let agent: ReturnType; + let tokenUser1: string; + + beforeAll(async () => { + const app = await createTestApp(); + agent = request.agent(app); + + const hashed = await bcrypt.hash('1234', 10); + jest.spyOn(prisma.user, 'findUnique').mockResolvedValue({ + id: 7, + username: 'u', + email: 'u@ex.com', + password: hashed, + images: [], + createdAt: new Date(), + updatedAt: new Date(), + }); + + const res = await agent + .post('/users/login') + .send({ username: 'u', password: '1234' }); + + expect(res.status).toBe(200); + tokenUser1 = res.body.accessToken as string; + }); + + test('PATCH /notifications/read-all → 200 {count:0}', async () => { + const app = await createTestApp(); + const { accessToken } = await loginAndGetSession(app, { userId: 77 }); + const res = await request(app) + .patch('/notifications/read-all') + .set('Authorization', `Bearer ${accessToken}`) + .expect((r) => { + if (![200, 204].includes(r.status)) { + throw new Error(`expected 200 or 204, got ${r.status}`); + } + }); + + if (res.status === 200) { + expect(res.body).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ count: expect.any(Number) }), + }) + ); + } + }); + + test('GET /notifications -> 내 알림 목록', async () => { + const res = await agent + .get('/notifications') + .set('Authorization', `Bearer ${tokenUser1}`); + + expect(res.status).toBe(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + test('GET /notifications/unread-count -> 미읽음 개수', async () => { + const res = await agent + .get('/notifications/unread-count') + .set('Authorization', `Bearer ${tokenUser1}`); + + expect(res.status).toBe(200); + expect(res.body).toEqual( + expect.objectContaining({ unreadCount: expect.any(Number) }) + ); + }); + + test('PATCH /notifications/:id/read -> 단건 읽음 처리', async () => { + const list = await agent + .get('/notifications') + .set('Authorization', `Bearer ${tokenUser1}`); + + const target = list.body.items?.[0]; + if (!target) return; + + const res = await agent + .patch(`/notifications/${target.id}/read`) + .set('Authorization', `Bearer ${tokenUser1}`); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ message: '알림이 읽음 처리되었습니다.' }); + }); + + test('PATCH /notifications/read-all -> 전체 읽음 처리', async () => { + const res = await agent + .patch('/notifications/read-all') + .set('Authorization', `Bearer ${tokenUser1}`); + + expect(res.status).toBe(204); + expect(res.text === '' || !res.text).toBe(true); + }); + + test('보호 API: 토큰 없으면 401', async () => { + const res = await agent.get('/notifications'); + expect(res.status).toBe(401); + }); +}); diff --git a/part4-mission11/tests/int/int.products.public.test.ts b/part4-mission11/tests/int/int.products.public.test.ts new file mode 100644 index 000000000..87d2a545d --- /dev/null +++ b/part4-mission11/tests/int/int.products.public.test.ts @@ -0,0 +1,84 @@ +import request from 'supertest'; + +import { + prismaReset, + seedProducts, + seedProductLikes, + seedCommentLikes, +} from '../_helper/prisma-mock.js'; +import { createTestApp } from '../_helper/test-app.js'; + +describe('[통합] 게시글 API (비인증)', () => { + let app: import('express').Express; + + beforeAll(async () => { + app = await createTestApp(); + }); + + beforeEach(() => { + prismaReset(); + seedProducts([ + { + id: 21, + name: 'B', + userId: 101, + price: 999, + images: ['b1.png'], + tags: [], + }, + { + id: 22, + name: 'C', + userId: 101, + price: 999, + images: ['c1.png'], + tags: [], + }, + ]); + seedProductLikes([ + { productId: 21, userId: 1 }, + { productId: 21, userId: 2 }, + ]); + seedCommentLikes([{ commentId: 100, userId: 1 }]); + }); + + test('GET /products?query=__not_exists__ → 200 []', async () => { + const app = await createTestApp(); + seedProducts([{ id: 1, name: 'Alpha', price: 1, userId: 1 }]); + + const res = await request(app) + .get('/products?query=__not_exists__') + .expect(200); + expect(Array.isArray(res.body.data)).toBe(true); + expect(res.body.data.length).toBe(0); + }); + + test('GET /products → 200 + 목록', async () => { + const res = await request(app) + .get('/products') + .query({ page: 1, pageSize: 10 }) + .expect(200); + + expect(res.body).toEqual( + expect.objectContaining({ + data: expect.arrayContaining([ + expect.objectContaining({ id: 21, name: 'B' }), + expect.objectContaining({ id: 22, name: 'C' }), + ]), + }) + ); + }); + + test('GET /products/:id → 200 + 단건', async () => { + const res = await request(app).get('/products/21').expect(200); + expect(res.body).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ id: 21, name: 'B' }), + }) + ); + }); + + test('GET /products/404 → 404', async () => { + await request(app).get('/products/404').expect(404); + }); +}); diff --git a/part4-mission11/tests/int/int.products.secure.test.ts b/part4-mission11/tests/int/int.products.secure.test.ts new file mode 100644 index 000000000..589a00eec --- /dev/null +++ b/part4-mission11/tests/int/int.products.secure.test.ts @@ -0,0 +1,165 @@ +import '../_helper/mock-modules.js'; +import request from 'supertest'; + +import { prismaReset, seedProducts } from '../_helper/prisma-mock.js'; +import { createTestApp } from '../_helper/test-app.js'; +import { loginAndGetSession } from '../_helper/test-utils.js'; + +describe('[통합] 상품 API (인증 필요)', () => { + let app: import('express').Express; + + beforeAll(async () => { + app = await createTestApp(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + prismaReset(); + }); + + test('POST /products → 201 (쿠키 필요)', async () => { + const { accessToken } = await loginAndGetSession(app, { userId: 7 }); + + const res = await request(app) + .post('/products') + .set('Authorization', `Bearer ${accessToken}`) + .send({ name: 'N', description: 'd', price: 999, tags: [], images: [] }) + .expect(201); + + const createdId = res.body?.data?.id ?? 1; + + const getRes = await request(app).get(`/products/${createdId}`).expect(200); + expect(getRes.body.data).toEqual( + expect.objectContaining({ name: 'N', description: 'd' }) + ); + }); + + test('POST /products → 401 (토큰 없음)', async () => { + await request(app) + .post('/products') + .send({ name: 'N', description: 'd', price: 999, tags: [], images: [] }) + .expect(401); + }); + + test('POST /products → 400 (검증 실패: 빈 name)', async () => { + const { accessToken } = await loginAndGetSession(app); + + const res = await request(app) + .post('/products') + .set('Authorization', `Bearer ${accessToken}`) + .send({ name: '', description: 'd', price: 999, tags: [], images: [] }) + .expect(400); + + expect(res.body).toEqual( + expect.objectContaining({ + message: expect.stringMatching('상품 이름은 필수입니다.'), + }) + ); + }); + + test('PATCH /products/:id → 200 (쿠키 필요)', async () => { + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + seedProducts([ + { id: 1, name: 'N', description: 'd', price: 1000, userId: user.id }, + ]); + + await request(app) + .patch('/products/1') + .set('Authorization', `Bearer ${accessToken}`) + .send({ name: 'N2', description: 'd2', price: 888, tags: [], images: [] }) + .expect(200); + + const res = await request(app).get('/products/1').expect(200); + expect(res.body.data).toEqual( + expect.objectContaining({ + id: 1, + name: 'N2', + description: 'd2', + price: 888, + }) + ); + }); + + test('PATCH /products/:id → 403 (소유자 아님)', async () => { + prismaReset(); + seedProducts([ + { id: 2, name: 'X', description: 'd', price: 1000, userId: 999 }, + ]); + const { accessToken } = await loginAndGetSession(app, { userId: 7 }); + + const res = await request(app) + .patch('/products/2') + .set('Authorization', `Bearer ${accessToken}`) + .send({ name: 'X2', description: 'd2', price: 777, tags: [], images: [] }) + .expect(403); + + expect(res.body).toEqual( + expect.objectContaining({ + message: expect.stringMatching('권한이 없습니다.'), + }) + ); + }); + + test('DELETE /products/:id → 204 (쿠키 필요)', async () => { + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + seedProducts([ + { id: 1, name: 'N', description: 'd', price: 1000, userId: user.id }, + ]); + + await request(app) + .delete('/products/1') + .set('Authorization', `Bearer ${accessToken}`) + .expect(204); + }); + + test('DELETE /products/:id → 204 & 이후 조회 404', async () => { + prismaReset(); + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + seedProducts([ + { id: 3, name: 'T', description: 'd', price: 1000, userId: user.id }, + ]); + + await request(app) + .delete('/products/3') + .set('Authorization', `Bearer ${accessToken}`) + .expect(204); + + await request(app).get('/products/3').expect(404); + }); + + test('POST/DELETE /products/:id/like → 200 → 200', async () => { + prismaReset(); + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + seedProducts([ + { id: 10, name: 'L', description: 'd', price: 1000, userId: user.id }, + ]); + + const likeRes = await request(app) + .post('/products/10/like') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + expect(likeRes.body).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ + message: expect.stringMatching(/완료|liked/i), + likeCount: expect.any(Number), + }), + }) + ); + + const unlikeRes = await request(app) + .delete('/products/10/like') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + expect(unlikeRes.body).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ + message: expect.stringMatching(/취소|unliked/i), + likeCount: expect.any(Number), + }), + }) + ); + }); +}); diff --git a/part4-mission11/tests/int/int.uploads.test.ts b/part4-mission11/tests/int/int.uploads.test.ts new file mode 100644 index 000000000..873fae04f --- /dev/null +++ b/part4-mission11/tests/int/int.uploads.test.ts @@ -0,0 +1,119 @@ +import fs from 'fs'; +import path from 'path'; +import request from 'supertest'; + +describe('[통합] 이미지 업로드', () => { + let app: import('express').Express; + const projectRoot = path.resolve(process.cwd()); + const savedDir = path.join(projectRoot, 'src', 'uploads'); + + beforeAll(async () => { + const { createTestApp } = await import('../_helper/test-app.js'); + app = await createTestApp(); + + fs.mkdirSync(savedDir, { recursive: true }); + }); + + afterEach(() => { + if (!fs.existsSync(savedDir)) return; + for (const f of fs.readdirSync(savedDir)) { + try { + fs.unlinkSync(path.join(savedDir, f)); + } catch {} + } + }); + + test('POST /images/upload/single → 400 (파일 없음)', async () => { + await request(app).post('/images/upload/single').expect(400); + }); + + test('단일 업로드: 이미지(jpg) → 200 + 파일 생성', async () => { + const jpg = Buffer.from([0xff, 0xd8, 0xff, 0xd9]); + const res = await request(app) + .post('/images/upload/single') + .attach('myImage', jpg, { filename: 'a.jpg', contentType: 'image/jpeg' }) + .expect(200); + + expect(res.body).toEqual( + expect.objectContaining({ + message: expect.any(String), + filename: expect.stringMatching(/myImage-\d+\.jpg$/), + url: expect.stringMatching(/^\/uploads\/myImage-\d+\.jpg$/), + }) + ); + + const filename: string = res.body.filename; + const onDisk = path.join(savedDir, filename); + expect(fs.existsSync(onDisk)).toBe(true); + expect(fs.statSync(onDisk).size).toBeGreaterThan(0); + }); + + test('여러 파일 업로드(<=5개) → 200 + files 배열', async () => { + const png = Buffer.from([0x89, 0x50, 0x4e, 0x47]); + const req = request(app).post('/images/upload/array'); + for (let i = 0; i < 3; i++) { + req.attach('myImages', png, { + filename: `b${i}.png`, + contentType: 'image/png', + }); + } + const res = await req.expect(200); + + expect(res.body).toEqual( + expect.objectContaining({ + message: expect.any(String), + files: expect.any(Array), + }) + ); + expect(res.body.files).toHaveLength(3); + for (const f of res.body.files as Array<{ + filename: string; + url: string; + }>) { + expect(f.filename).toMatch(/^myImages-\d+\.png$/); + expect(f.url).toMatch(/^\/uploads\/myImages-\d+\.png$/); + } + }); + + test('허용되지 않는 형식(text/plain) → 400', async () => { + const txt = Buffer.from('hello'); + await request(app) + .post('/images/upload/single') + .attach('myImage', txt, { filename: 'c.txt', contentType: 'text/plain' }) + .expect(400); + }); + + test('파일 개수 초과(6개) → 400 (LIMIT_FILE_COUNT)', async () => { + const webp = Buffer.from('RIFF'); + let req = request(app).post('/images/upload/array'); + for (let i = 0; i < 6; i++) { + req = req.attach('myImages', webp, { + filename: `d${i}.webp`, + contentType: 'image/webp', + }); + } + const _res = await req.expect(400); + }); + + test('파일 크기 초과(>5MB) → 400 (LIMIT_FILE_SIZE)', async () => { + const big = Buffer.alloc(5 * 1024 * 1024 + 1, 0); // 5MB + 1 + const _res = await request(app) + .post('/images/upload/single') + .attach('myImage', big, { filename: 'e.jpg', contentType: 'image/jpeg' }) + .expect(400); + }); + + test('단일 업로드: 파일 미첨부 → 400', async () => { + await request(app) + .post('/images/upload/single') + .field('note', 'no file') + .expect(400); + }); + + test('여러 파일 업로드: 파일 미첨부 → 400', async () => { + await request(app) + .post('/images/upload/array') + .field('note', 'no files') + .expect(400); + }); +}); diff --git a/part4-mission11/tests/int/int.users.secure.test.ts b/part4-mission11/tests/int/int.users.secure.test.ts new file mode 100644 index 000000000..35fd437f7 --- /dev/null +++ b/part4-mission11/tests/int/int.users.secure.test.ts @@ -0,0 +1,192 @@ +// src/tests/int.users.secure.test.ts +import { jest } from '@jest/globals'; +import request from 'supertest'; + +import { + prismaReset, + seedArticles, + seedCommentLikes, + seedComments, + seedProductLikes, + seedProducts, + seedUsersWithHash, +} from '../_helper/prisma-mock.js'; +import { createTestApp } from '../_helper/test-app.js'; +import { loginAndGetSession } from '../_helper/test-utils.js'; + +describe('[통합] 유저 API (인증 필요)', () => { + let app: import('express').Express; + + beforeAll(async () => { + app = await createTestApp(); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + prismaReset(); + await seedUsersWithHash([ + { id: 7, username: 'u', email: 'u@ex.com', password: 'pw' }, + ]); + }); + + // ---------------------------- + // 프로필 수정 (PATCH /users/:userId) + // ---------------------------- + test('PATCH /users/:id → 200 (소유자)', async () => { + const { accessToken, user } = await loginAndGetSession(app, { userId: 7 }); + const res = await request(app) + .patch('/users/7') + .set('Authorization', `Bearer ${accessToken}`) + .send({ id: user.id, username: 'u', email: 'u@ex.com', images: [] }) + .expect(200); + + expect(res.body).toEqual( + expect.objectContaining({ + updated: expect.objectContaining({ + id: 7, + username: 'u', + email: 'u@ex.com', + images: [], + }), + message: '프로필 수정 완료!', + }) + ); + }); + + test('PATCH /users/:id → 403 (비소유자)', async () => { + const { accessToken } = await loginAndGetSession(app, { userId: 8 }); + await request(app) + .patch('/users/7') + .set('Authorization', `Bearer ${accessToken}`) + .send({ username: 'hack' }) + .expect(403); + }); + + // ---------------------------- + // 비밀번호 변경 (PATCH /users/:userId/password) + // ---------------------------- + test('PATCH /users/:id/password → 400 (새 비번 불일치)', async () => { + const { accessToken } = await loginAndGetSession(app, { userId: 7 }); + const res = await request(app) + .patch('/users/7/password') + .set('Authorization', `Bearer ${accessToken}`) + .send({ + currentPassword: 'pw', + newPassword: 'n1', + newPasswordConfirm: 'n2', + }) + .expect(400); + + expect(typeof res.body.message).toBe('string'); + expect(res.body.message).toEqual( + expect.stringMatching(/(일치하지 않습니다|불일치|mismatch|confirm)/i) + ); + }); + + test('PATCH /users/:id/password → 200 (소유자)', async () => { + const { accessToken } = await loginAndGetSession(app, { userId: 7 }); + const res = await request(app) + .patch('/users/7/password') + .set('Authorization', `Bearer ${accessToken}`) + .send({ + currentPassword: 'pw', + newPassword: 'NewPass123!', + newPasswordConfirm: 'NewPass123!', + }) + .expect(200); + + expect(res.body).toEqual({ message: '비밀번호가 변경되었습니다.' }); + }); + + // ---------------------------- + // 내가 단 댓글 (GET /users/:userId/my-comments) + // ---------------------------- + test('GET /users/:id/my-comments → 200 + {comments}', async () => { + const { accessToken } = await loginAndGetSession(app, { userId: 7 }); + + // 시드 + seedArticles([{ id: 101, title: 't1', userId: 7 }]); + seedProducts([{ id: 202, name: 'p1', userId: 7, price: 1000 }]); + seedComments([ + { id: 1, userId: 7, content: 'hi', articleId: 101 }, + { id: 2, userId: 7, content: 'hello', productId: 202 }, + ]); + seedCommentLikes([ + { commentId: 2, userId: 100 }, + { commentId: 2, userId: 101 }, + { commentId: 2, userId: 102 }, + ]); + + const res = await request(app) + .get('/users/7/my-comments') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + expect(Array.isArray(res.body.comments)).toBe(true); + expect(res.body.comments).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 1, + content: 'hi', + userId: 7, + articleId: 101, + productId: null, + likeCount: expect.any(Number), + }), + expect.objectContaining({ + id: 2, + content: 'hello', + userId: 7, + articleId: null, + productId: 202, + likeCount: 3, + }), + ]) + ); + }); + + // ---------------------------- + // 좋아요한 목록들 + // ---------------------------- + test('GET /users/:id/likes/products → 200 + {data}', async () => { + const { accessToken } = await loginAndGetSession(app, { userId: 7 }); + + seedProducts([{ id: 10, name: 'Prod10', userId: 99, price: 1000 }]); + seedProductLikes([{ productId: 10, userId: 7 }]); + + const res = await request(app) + .get('/users/7/likes/products') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + expect(Array.isArray(res.body.data)).toBe(true); + expect(res.body.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 10, name: 'Prod10' }), + ]) + ); + }); + + test('GET /users/:id/likes/comments → 200 + {data}', async () => { + const { accessToken } = await loginAndGetSession(app, { userId: 7 }); + + seedArticles([{ id: 20, title: 'A20', userId: 88 }]); + seedComments([{ id: 30, userId: 55, content: 'nice', articleId: 20 }]); + seedCommentLikes([{ commentId: 30, userId: 7 }]); + + const res = await request(app) + .get('/users/7/likes/comments') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + expect(Array.isArray(res.body.data)).toBe(true); + expect(res.body.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 30, + content: 'nice', + }), + ]) + ); + }); +}); diff --git a/part4-mission11/tests/int/int.ws.test.ts b/part4-mission11/tests/int/int.ws.test.ts new file mode 100644 index 000000000..44390f64d --- /dev/null +++ b/part4-mission11/tests/int/int.ws.test.ts @@ -0,0 +1,123 @@ +import { NotificationType as P } from '@prisma/client'; +import { createServer } from 'http'; +import type { AddressInfo } from 'net'; +import { io as Client, type Socket } from 'socket.io-client'; + +import { + setupWebSocket, + wsGateway, + closeWebSocket, + __resetWsForTest, + __getUserSocketsForTest, +} from '../../src/lib/ws.js'; + +/** ---- 타입 ---- */ +type Wire = { + type: 'system' | 'chat' | 'contract-linked'; + message: string; + createdAt: string; + data?: Record; +}; +type S2C = { + joined: (v: { ok: boolean; userId: number }) => void; + notification: (p: Wire) => void; + error: (m: string) => void; +}; +type C2S = Record; + +/** ---- 헬퍼 ---- */ +const waitConnect = (s: Socket) => + new Promise((res, rej) => { + const ok = () => { + cleanup(); + res(); + }; + const er = (e: unknown) => { + cleanup(); + rej(e); + }; + const cleanup = () => { + s.off('connect', ok); + s.off('connect_error', er as (e: Error) => void); + }; + s.once('connect', ok); + s.once('connect_error', er as (e: Error) => void); + }); + +const onceNotification = (s: Socket) => + new Promise((resolve) => { + const h = (p: Wire) => { + s.off('notification', h); + resolve(p); + }; + s.on('notification', h); + }); + +// 서버 내부 소켓맵에서 특정 userId의 연결 개수가 기대치가 될 때까지 대기 +async function waitForServerSockets( + userId: number, + expectedCount: number, + timeoutMs = 5000 +) { + const start = Date.now(); + // 폴링 주기 50ms + while (Date.now() - start < timeoutMs) { + const map = __getUserSocketsForTest(); + const set = map.get(userId); + if (set && set.size === expectedCount) return; + await new Promise((r) => setTimeout(r, 50)); + } + throw new Error( + `userSockets(${userId}) did not reach ${expectedCount} within ${timeoutMs}ms` + ); +} + +/** ---- 상태 초기화 ---- */ +beforeEach(() => __resetWsForTest()); +afterEach(async () => { + await closeWebSocket(); + __resetWsForTest(); +}); + +/** ---- 본 테스트 ---- */ +test('setupWebSocket + wsGateway.notifyUser → client receives', async () => { + const httpServer = createServer(); + httpServer.keepAliveTimeout = 0; + httpServer.headersTimeout = 0; + + await new Promise((r) => httpServer.listen(0, '127.0.0.1', r)); + const { port } = httpServer.address() as AddressInfo; + const base = `http://127.0.0.1:${port}`; + + setupWebSocket(httpServer, { auth: () => 99 }); + await new Promise((r) => setImmediate(r)); + + const opts = { + path: '/ws', + auth: { token: 'T' }, + transports: ['websocket'], + reconnection: false, + forceNew: true, + timeout: 4000, + }; + + const c1 = Client(base, opts) as unknown as Socket; + const c2 = Client(base, opts) as unknown as Socket; + + try { + await Promise.all([waitConnect(c1), waitConnect(c2)]); + await waitForServerSockets(99, 2, 5000); + + const p = onceNotification(c2); + wsGateway.notifyUser({ userId: 99, type: P.NEW_COMMENT, message: 'hi' }); + const got = await p; + expect(got).toMatchObject({ message: 'hi' }); + } finally { + c1.removeAllListeners(); + c2.removeAllListeners(); + c1.disconnect(); + c2.disconnect(); + await closeWebSocket(); + await new Promise((r) => httpServer.close(() => r())); + } +}, 20000); diff --git a/part4-mission11/tests/unit/unit.article-comment.service.test.ts b/part4-mission11/tests/unit/unit.article-comment.service.test.ts new file mode 100644 index 000000000..94e9a3aba --- /dev/null +++ b/part4-mission11/tests/unit/unit.article-comment.service.test.ts @@ -0,0 +1,323 @@ +import { + describe, + test, + expect, + beforeEach, + afterEach, + jest, +} from '@jest/globals'; + +import AppError from '../../src/lib/appError.js'; +import { articleRepository } from '../../src/repositories/article-repository.js'; +import { articleCommentRepository } from '../../src/repositories/comments/article-comment-repository.js'; +import { commentRepository } from '../../src/repositories/comments/comment-repository.js'; +import { commentLikeRepository } from '../../src/repositories/like-repository.js'; +import { notificationRepository } from '../../src/repositories/notification-repository.js'; +import { userRepository } from '../../src/repositories/user-repository.js'; +import { articleCommentService } from '../../src/services/comments/article-comment-service.js'; +import { notificationService } from '../../src/services/notification-service.js'; +import { + makeArticleLite, + makeComment, + makeNotification, + makeListedComment, + makeCommentLike, +} from '../_helper/factories.js'; +import prisma from '../_helper/prisma-mock.js'; + +describe('ArticleCommentService', () => { + const ARTICLE_ID = 10; + const AUTHOR_ID = 1; + const OTHER_USER_ID = 2; + + beforeEach(async () => jest.restoreAllMocks()); + afterEach(async () => jest.clearAllMocks()); + + test('getCommentsByArticleId: likeCount / isLiked 포함', async () => { + jest + .spyOn(articleCommentRepository, 'findByArticleId') + .mockResolvedValue([ + makeListedComment({ id: 101, content: 'c1', user: { username: 'u1' } }), + makeListedComment({ id: 102, content: 'c2', user: { username: 'u2' } }), + ]); + + jest.spyOn(commentLikeRepository, 'countByTargetIds').mockResolvedValue([ + { commentId: 101, _count: { commentId: 3 } }, + { commentId: 102, _count: { commentId: 0 } }, + ]); + jest + .spyOn(commentLikeRepository, 'findByUserAndTargetIds') + .mockImplementation(async (uid: number, _ids: number[]) => + uid === OTHER_USER_ID ? [{ commentId: 101 }] : [] + ); + + const list = await articleCommentService.getCommentsByArticleId( + ARTICLE_ID, + OTHER_USER_ID + ); + + expect(list).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 101, likeCount: 3, isLiked: true }), + expect.objectContaining({ id: 102, likeCount: 0, isLiked: false }), + ]) + ); + }); + + test('createArticleComment: 글 없음 → 404', async () => { + jest.spyOn(articleRepository, 'findLiteById').mockResolvedValue(null); + await expect( + articleCommentService.createArticleComment( + ARTICLE_ID, + 'hello', + OTHER_USER_ID + ) + ).rejects.toThrow(AppError); + }); + + test('createArticleComment: 자기 글이면 알림 스킵', async () => { + jest + .spyOn(articleRepository, 'findLiteById') + .mockResolvedValue( + makeArticleLite({ id: ARTICLE_ID, userId: AUTHOR_ID, title: 'T' }) + ); + jest + .spyOn(articleCommentRepository, 'create') + .mockResolvedValue( + makeComment({ id: 201, articleId: ARTICLE_ID, userId: AUTHOR_ID }) + ); + + const { notificationService } = await import( + '../../src/services/notification-service.js' + ); + const pushSpy = jest + .spyOn(notificationService, 'pushArticleComment') + .mockResolvedValue( + makeNotification({ + userId: AUTHOR_ID, + articleId: ARTICLE_ID, + commentId: 201, + type: 'NEW_COMMENT', + }) + ); + + await articleCommentService.createArticleComment( + ARTICLE_ID, + 'c', + AUTHOR_ID + ); + + expect(pushSpy).not.toHaveBeenCalled(); + }); + + test('createArticleComment: 남의 글이면 알림 발송', async () => { + jest + .spyOn(articleRepository, 'findLiteById') + .mockResolvedValue( + makeArticleLite({ id: ARTICLE_ID, userId: AUTHOR_ID, title: 'T' }) + ); + jest + .spyOn(articleCommentRepository, 'create') + .mockResolvedValue( + makeComment({ id: 202, articleId: ARTICLE_ID, userId: OTHER_USER_ID }) + ); + jest + .spyOn(userRepository, 'findUsernameById') + .mockResolvedValue({ username: '댓글쓴이' }); + + const { notificationService } = await import( + '../../src/services/notification-service.js' + ); + const pushSpy = jest + .spyOn(notificationService, 'pushArticleComment') + .mockResolvedValue( + makeNotification({ + id: 999, + userId: AUTHOR_ID, + articleId: ARTICLE_ID, + commentId: 202, + type: 'NEW_COMMENT', + message: '새 댓글이 달렸습니다.', + }) + ); + + await articleCommentService.createArticleComment( + ARTICLE_ID, + 'c', + OTHER_USER_ID + ); + + expect(pushSpy).toHaveBeenCalledWith( + expect.objectContaining({ + receiverUserId: AUTHOR_ID, + articleId: ARTICLE_ID, + commentId: 202, + articleTitle: 'T', + commenterName: '댓글쓴이', + }) + ); + }); + + test('pushArticleComment: 정상', async () => { + const repoSpy = jest + .spyOn(notificationRepository, 'create') + .mockResolvedValue( + makeNotification({ + id: 1, + userId: 42, + articleId: 10, + commentId: 777, + message: '새 댓글', + }) + ); + + const res = await notificationService.pushArticleComment({ + receiverUserId: 42, + articleId: 10, + commentId: 777, + articleTitle: 'T', + commenterName: 'A', + }); + + expect(repoSpy).toHaveBeenCalledWith( + expect.objectContaining({ + userId: 42, + articleId: 10, + commentId: 777, + message: expect.any(String), + }) + ); + expect(res.id).toBe(1); + }); + + test('markAsRead: 정상', async () => { + const upd = jest + .spyOn(prisma.notification, 'updateMany') + .mockResolvedValue({ count: 1 }); + + await expect( + notificationService.markAsRead(99, 7) + ).resolves.toBeUndefined(); + + expect(upd).toHaveBeenCalledWith({ + where: { id: 7, userId: 99 }, + data: { isRead: true }, + }); + }); + + test('markAsRead: 알림이 없거나 내 것이 아니면 404', async () => { + jest + .spyOn(prisma.notification, 'updateMany') + .mockResolvedValue({ count: 0 }); + + await expect(notificationService.markAsRead(99, 7)).rejects.toThrow( + '알림을 찾을 수 없습니다.' + ); + }); + + test('updateComment: 본인 아니면 403', async () => { + jest + .spyOn(commentRepository, 'findById') + .mockResolvedValue(makeComment({ id: 301, userId: AUTHOR_ID })); + await expect( + articleCommentService.updateComment(301, OTHER_USER_ID, 'x') + ).rejects.toThrow(AppError); + }); + + test('updateComment: 성공', async () => { + jest + .spyOn(commentRepository, 'findById') + .mockResolvedValue(makeComment({ id: 301, userId: AUTHOR_ID })); + const upd = jest + .spyOn(commentRepository, 'update') + .mockResolvedValue( + makeComment({ id: 301, content: 'new', userId: AUTHOR_ID }) + ); + + const r = await articleCommentService.updateComment(301, AUTHOR_ID, 'new'); + expect(upd).toHaveBeenCalledWith(301, 'new'); + expect(r.content).toBe('new'); + }); + + test('pushNewComment: 정상', async () => { + const repoSpy = jest + .spyOn(notificationRepository, 'create') + .mockResolvedValue( + makeNotification({ + id: 1, + userId: 42, + articleId: 10, + commentId: 777, + message: '새 댓글', + }) + ); + + const res = await notificationService.pushArticleComment({ + receiverUserId: 42, + articleId: 10, + commentId: 777, + articleTitle: 'T', + commenterName: 'A', + }); + + expect(repoSpy).toHaveBeenCalledWith( + expect.objectContaining({ + userId: 42, + articleId: 10, + commentId: 777, + message: expect.any(String), + }) + ); + expect(res.id).toBe(1); + }); + + test('deleteComment: 0건 삭제 → 403', async () => { + jest.spyOn(commentRepository, 'delete').mockResolvedValue({ count: 0 }); + await expect( + articleCommentService.deleteComment(301, OTHER_USER_ID) + ).rejects.toThrow(AppError); + }); + + test('deleteComment: 성공', async () => { + jest.spyOn(commentRepository, 'delete').mockResolvedValue({ count: 1 }); + await expect( + articleCommentService.deleteComment(301, AUTHOR_ID) + ).resolves.toEqual( + expect.objectContaining({ message: '댓글이 삭제되었습니다.' }) + ); + }); + + test('commentLike / commentUnlike 플로우', async () => { + jest.spyOn(commentLikeRepository, 'exists').mockResolvedValueOnce(true); + await expect( + articleCommentService.commentLike(OTHER_USER_ID, 500) + ).rejects.toThrow(AppError); + + jest.spyOn(commentLikeRepository, 'exists').mockResolvedValueOnce(false); + const create = jest + .spyOn(commentLikeRepository, 'create') + .mockResolvedValue( + makeCommentLike({ userId: OTHER_USER_ID, commentId: 500 }) + ); + jest.spyOn(commentLikeRepository, 'count').mockResolvedValue(7); + const liked = await articleCommentService.commentLike(OTHER_USER_ID, 500); + expect(create).toHaveBeenCalled(); + expect(liked.likeCount).toBe(7); + + jest.spyOn(commentLikeRepository, 'exists').mockResolvedValueOnce(false); + await expect( + articleCommentService.commentUnlike(OTHER_USER_ID, 500) + ).rejects.toThrow(AppError); + + jest.spyOn(commentLikeRepository, 'exists').mockResolvedValueOnce(true); + const del = jest + .spyOn(commentLikeRepository, 'delete') + .mockResolvedValue( + makeCommentLike({ userId: OTHER_USER_ID, commentId: 500 }) + ); + jest.spyOn(commentLikeRepository, 'count').mockResolvedValue(6); + const un = await articleCommentService.commentUnlike(OTHER_USER_ID, 500); + expect(del).toHaveBeenCalled(); + expect(un.likeCount).toBe(6); + }); +}); diff --git a/part4-mission11/tests/unit/unit.auth.localStrategy.test.ts b/part4-mission11/tests/unit/unit.auth.localStrategy.test.ts new file mode 100644 index 000000000..407fcfecd --- /dev/null +++ b/part4-mission11/tests/unit/unit.auth.localStrategy.test.ts @@ -0,0 +1,79 @@ +// src/tests/auth.localStrategy.int.test.ts +import bcrypt from 'bcrypt'; +import request from 'supertest'; + +import { localStrategy } from '../../src/lib/passport/localStrategy.js'; +import { prisma } from '../../src/lib/prismaClient.js'; +import { createPassportTestApp } from '../_helper/test-passport-app.js'; + +type Done = (err: unknown, user?: unknown, info?: unknown) => void; +type VerifyFn = ( + username: string, + password: string, + done: Done +) => void | Promise; + +describe('passport-local (비밀번호 검증)', () => { + const app = createPassportTestApp(); + const verify: VerifyFn = (localStrategy as unknown as { _verify: VerifyFn }) + ._verify; + test('localStrategy: 유저 없음 → 실패', async () => { + // prisma.user.findUnique 가 null 반환하도록 모킹 + jest.spyOn(prisma.user, 'findUnique').mockResolvedValueOnce(null); + const done = jest.fn(); + await verify('nouser', 'pw', done); + expect(done).toHaveBeenCalledWith(null, false); + }); + + test('localStrategy: 비밀번호 불일치 → 실패', async () => { + const hashed = await bcrypt.hash('right', 10); + jest.spyOn(prisma.user, 'findUnique').mockResolvedValueOnce({ + id: 1, + username: 'u', + email: 'e', + password: hashed, + images: [], + createdAt: new Date(), + updatedAt: new Date(), + }); + const done = jest.fn(); + await verify('u', 'wrong', done); + expect(done).toHaveBeenCalledWith(null, false); + }); + + test('비번이 맞으면 200', async () => { + const hashed = await bcrypt.hash('1234abcd!', 10); + jest.spyOn(prisma.user, 'findUnique').mockResolvedValue({ + id: 7, + username: 'u', + email: 'u@ex.com', + password: hashed, + createdAt: new Date(), + updatedAt: new Date(), + images: [], + }); + + await request(app) + .post('/login') + .send({ username: 'u', password: '1234abcd!' }) + .expect(200); + }); + + test('비번이 틀리면 401', async () => { + const hashed = await bcrypt.hash('1234abcd!', 10); + jest.spyOn(prisma.user, 'findUnique').mockResolvedValue({ + id: 7, + username: 'u', + email: 'u@ex.com', + password: hashed, + createdAt: new Date(), + updatedAt: new Date(), + images: [], + }); + + await request(app) + .post('/login') + .send({ username: 'u', password: 'wrong!' }) + .expect(401); + }); +}); diff --git a/part4-mission11/tests/unit/unit.product-comment.service.test.ts b/part4-mission11/tests/unit/unit.product-comment.service.test.ts new file mode 100644 index 000000000..3464720cb --- /dev/null +++ b/part4-mission11/tests/unit/unit.product-comment.service.test.ts @@ -0,0 +1,287 @@ +import { + describe, + test, + expect, + beforeEach, + afterEach, + jest, +} from '@jest/globals'; + +import AppError from '../../src/lib/appError.js'; +import { commentRepository } from '../../src/repositories/comments/comment-repository.js'; +import { productCommentRepository } from '../../src/repositories/comments/product-comment-repository.js'; +import { commentLikeRepository } from '../../src/repositories/like-repository.js'; +import { notificationRepository } from '../../src/repositories/notification-repository.js'; +import { productRepository } from '../../src/repositories/product-repository.js'; +import { userRepository } from '../../src/repositories/user-repository.js'; +import { productCommentService } from '../../src/services/comments/product-comment-service.js'; +import { notificationService } from '../../src/services/notification-service.js'; +import { + makeProductLite, + makeComment, + makeListedComment, + makeNotification, + makeCommentLike, +} from '../_helper/factories.js'; +import prisma from '../_helper/prisma-mock.js'; + +describe('ProductCommentService', () => { + const PRODUCT_ID = 20; + const OWNER_ID = 10; + const OTHER_USER_ID = 11; + + beforeEach(async () => jest.restoreAllMocks()); + afterEach(async () => jest.clearAllMocks()); + + test('getCommentsByProductId: likeCount / isLiked 포함', async () => { + jest + .spyOn(productCommentRepository, 'findByProductId') + .mockResolvedValue([ + makeListedComment({ id: 901, content: 'p1', user: { username: 'u' } }), + ]); + jest + .spyOn(commentLikeRepository, 'countByTargetIds') + .mockResolvedValue([{ commentId: 901, _count: { commentId: 2 } }]); + + jest + .spyOn(commentLikeRepository, 'findByUserAndTargetIds') + .mockImplementation(async (uid: number, _ids: number[]) => + uid === OTHER_USER_ID ? [{ commentId: 901 }] : [] + ); + + const list = await productCommentService.getCommentsByProductId( + PRODUCT_ID, + OTHER_USER_ID + ); + expect(list[0]).toEqual( + expect.objectContaining({ id: 901, likeCount: 2, isLiked: true }) + ); + }); + + test('createProductComment: 상품 없음 → 404', async () => { + jest.spyOn(productRepository, 'findLiteById').mockResolvedValue(null); + await expect( + productCommentService.createProductComment(PRODUCT_ID, 'c', OTHER_USER_ID) + ).rejects.toThrow(AppError); + }); + + test('createProductComment: 내 상품이면 알림 스킵', async () => { + jest + .spyOn(productRepository, 'findLiteById') + .mockResolvedValue( + makeProductLite({ id: PRODUCT_ID, userId: OWNER_ID, name: 'N' }) + ); + jest + .spyOn(productCommentRepository, 'create') + .mockResolvedValue( + makeComment({ id: 777, productId: PRODUCT_ID, userId: OWNER_ID }) + ); + + const { notificationService } = await import( + '../../src/services/notification-service.js' + ); + const pushSpy = jest + .spyOn(notificationService, 'pushProductComment') + .mockResolvedValue( + makeNotification({ + userId: OWNER_ID, + productId: PRODUCT_ID, + commentId: 777, + type: 'NEW_COMMENT', + }) + ); + + await productCommentService.createProductComment(PRODUCT_ID, 'c', OWNER_ID); + expect(pushSpy).not.toHaveBeenCalled(); + }); + + test('createProductComment: 남의 상품이면 알림 발송', async () => { + jest + .spyOn(productRepository, 'findLiteById') + .mockResolvedValue( + makeProductLite({ id: PRODUCT_ID, userId: OWNER_ID, name: 'Mac' }) + ); + jest + .spyOn(productCommentRepository, 'create') + .mockResolvedValue( + makeComment({ id: 778, productId: PRODUCT_ID, userId: OTHER_USER_ID }) + ); + jest + .spyOn(userRepository, 'findUsernameById') + .mockResolvedValue({ username: 'buyer' }); + + const { notificationService } = await import( + '../../src/services/notification-service.js' + ); + const pushSpy = jest + .spyOn(notificationService, 'pushProductComment') + .mockResolvedValue( + makeNotification({ + id: 1000, + userId: OWNER_ID, + productId: PRODUCT_ID, + commentId: 778, + type: 'NEW_COMMENT', + message: '상품에 새 댓글이 달렸습니다.', + }) + ); + + await productCommentService.createProductComment( + PRODUCT_ID, + 'c', + OTHER_USER_ID + ); + + expect(pushSpy).toHaveBeenCalledWith( + expect.objectContaining({ + receiverUserId: OWNER_ID, + productId: PRODUCT_ID, + commentId: 778, + productName: 'Mac', + commenterName: 'buyer', + }) + ); + }); + + test('markAsRead: 정상', async () => { + const upd = jest + .spyOn(prisma.notification, 'updateMany') + .mockResolvedValue({ count: 1 }); + + await expect( + notificationService.markAsRead(99, 7) + ).resolves.toBeUndefined(); + + expect(upd).toHaveBeenCalledWith({ + where: { id: 7, userId: 99 }, + data: { isRead: true }, + }); + }); + + test('markAsRead: 알림이 없거나 내 것이 아니면 404', async () => { + jest + .spyOn(prisma.notification, 'updateMany') + .mockResolvedValue({ count: 0 }); + + await expect(notificationService.markAsRead(99, 7)).rejects.toThrow( + '알림을 찾을 수 없습니다.' + ); + }); + + test('updateComment: 본인 아니면 403', async () => { + jest + .spyOn(commentRepository, 'findById') + .mockResolvedValue( + makeComment({ id: 1, userId: OWNER_ID, productId: PRODUCT_ID }) + ); + await expect( + productCommentService.updateComment(1, OTHER_USER_ID, 'x') + ).rejects.toThrow(AppError); + }); + + test('updateComment: 성공', async () => { + jest + .spyOn(commentRepository, 'findById') + .mockResolvedValue( + makeComment({ id: 1, userId: OTHER_USER_ID, productId: PRODUCT_ID }) + ); + const upd = jest.spyOn(commentRepository, 'update').mockResolvedValue( + makeComment({ + id: 1, + content: 'x', + userId: OTHER_USER_ID, + productId: PRODUCT_ID, + }) + ); + + const r = await productCommentService.updateComment(1, OTHER_USER_ID, 'x'); + expect(upd).toHaveBeenCalledWith(1, 'x'); + expect(r.content).toBe('x'); + }); + + test('pushNewComment: 정상', async () => { + const repoSpy = jest + .spyOn(notificationRepository, 'create') + .mockResolvedValue( + makeNotification({ + id: 1, + userId: 42, + productId: 10, + commentId: 777, + message: '새 댓글' /* type 생략 가능 */, + }) + ); + + const res = await notificationService.pushProductComment({ + receiverUserId: 42, + productId: 10, + commentId: 777, + productName: 'N', + commenterName: 'P', + }); + + expect(repoSpy).toHaveBeenCalledWith( + expect.objectContaining({ + userId: 42, + productId: 10, + commentId: 777, + message: expect.any(String), + }) + ); + expect(res.id).toBe(1); + }); + + test('deleteComment: 0건 삭제 → 403', async () => { + jest.spyOn(commentRepository, 'delete').mockResolvedValue({ count: 0 }); + await expect( + productCommentService.deleteComment(1, OTHER_USER_ID) + ).rejects.toThrow(AppError); + }); + + test('deleteComment: 성공', async () => { + jest.spyOn(commentRepository, 'delete').mockResolvedValue({ count: 1 }); + await expect( + productCommentService.deleteComment(1, OTHER_USER_ID) + ).resolves.toEqual( + expect.objectContaining({ message: '댓글이 삭제되었습니다.' }) + ); + }); + + test('commentLike / commentUnlike 플로우', async () => { + // like: 중복 → 에러 + jest.spyOn(commentLikeRepository, 'exists').mockResolvedValueOnce(true); + await expect( + productCommentService.commentLike(OTHER_USER_ID, 500) + ).rejects.toThrow(AppError); + + // like: 정상 (create → Like 객체 반환) + jest.spyOn(commentLikeRepository, 'exists').mockResolvedValueOnce(false); + const create = jest + .spyOn(commentLikeRepository, 'create') + .mockResolvedValue( + makeCommentLike({ userId: OTHER_USER_ID, commentId: 500 }) + ); + jest.spyOn(commentLikeRepository, 'count').mockResolvedValue(8); + const liked = await productCommentService.commentLike(OTHER_USER_ID, 500); + expect(create).toHaveBeenCalled(); + expect(liked.likeCount).toBe(8); + + // unlike: 기록 없음 → 에러 + jest.spyOn(commentLikeRepository, 'exists').mockResolvedValueOnce(false); + await expect( + productCommentService.commentUnlike(OTHER_USER_ID, 500) + ).rejects.toThrow(AppError); + + // unlike: 정상 (delete → Like 객체 반환) + jest.spyOn(commentLikeRepository, 'exists').mockResolvedValueOnce(true); + const del = jest + .spyOn(commentLikeRepository, 'delete') + .mockResolvedValue( + makeCommentLike({ userId: OTHER_USER_ID, commentId: 500 }) + ); + jest.spyOn(commentLikeRepository, 'count').mockResolvedValue(7); + const un = await productCommentService.commentUnlike(OTHER_USER_ID, 500); + expect(del).toHaveBeenCalled(); + expect(un.likeCount).toBe(7); + }); +}); diff --git a/part4-mission11/tests/unit/unit.user-service.test.ts b/part4-mission11/tests/unit/unit.user-service.test.ts new file mode 100644 index 000000000..83a1462ad --- /dev/null +++ b/part4-mission11/tests/unit/unit.user-service.test.ts @@ -0,0 +1,407 @@ +import { describe, test, expect, beforeEach, jest } from '@jest/globals'; +import bcrypt from 'bcrypt'; + +import AppError from '../../src/lib/appError.js'; +import { + ACCESS_TOKEN_COOKIE_NAME, + REFRESH_TOKEN_COOKIE_NAME, +} from '../../src/lib/constants.js'; +import * as token from '../../src/lib/token.js'; +import * as likeRepo from '../../src/repositories/like-repository.js'; +import * as userRepo from '../../src/repositories/user-repository.js'; +import { userService } from '../../src/services/user-service.js'; + +/* ────────────────────────────────────────── + 시그니처를 그대로 따르는 안전한 스파이 + (bcrypt는 spy/mocking 금지: 실제 사용) + ────────────────────────────────────────── */ +const spyFindByUsername = jest.spyOn( + userRepo.userRepository, + 'findByUsername' +) as jest.SpiedFunction; + +const spyCreateUser = jest.spyOn( + userRepo.userRepository, + 'createUser' +) as jest.SpiedFunction; + +const spyFindById = jest.spyOn( + userRepo.userRepository, + 'findById' +) as jest.SpiedFunction; + +const spyFindByIdWithPassword = jest.spyOn( + userRepo.userRepository, + 'findByIdWithPassword' +) as jest.SpiedFunction; + +const spyUpdateUser = jest.spyOn( + userRepo.userRepository, + 'updateUser' +) as jest.SpiedFunction; + +const spyUpdatePassword = jest.spyOn( + userRepo.userRepository, + 'updatePassword' +) as jest.SpiedFunction; + +const spyGetUserComments = jest.spyOn( + userRepo.userRepository, + 'getUserComments' +) as jest.SpiedFunction; + +const spyGetUserLikedComments = jest.spyOn( + userRepo.userRepository, + 'getUserLikedComments' +) as jest.SpiedFunction; + +const spyLikeCount = jest.spyOn( + likeRepo.commentLikeRepository, + 'count' +) as jest.SpiedFunction; + +const mockGenerateTokens = token.generateTokens as jest.MockedFunction< + typeof token.generateTokens +>; +const mockVerifyRefresh = token.verifyRefreshToken as jest.MockedFunction< + typeof token.verifyRefreshToken +>; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('UserService', () => { + /* ───────── register ───────── */ + describe('register', () => { + test('새 유저 등록 → 비밀번호 해시 비공개 필드 제거', async () => { + spyFindByUsername.mockResolvedValue(null); + + // 실제 bcrypt로 해시를 만들어서 createUser 리턴에 넣음 + const hashed = await bcrypt.hash('1234', 10); + + type CreatedUser = Awaited< + ReturnType + >; + const createdUser: CreatedUser = { + id: 1, + username: 'u', + email: 'u@ex.com', + password: hashed, + images: [], + createdAt: new Date(), + updatedAt: new Date(), + } as CreatedUser; + + spyCreateUser.mockResolvedValue(createdUser); + + const out = await userService.register('u', 'u@ex.com', '1234'); + + expect(spyFindByUsername).toHaveBeenCalledWith('u'); + // 해시는 내부에서 수행되므로 createUser 인자만 검증 + expect(spyCreateUser).toHaveBeenCalledWith({ + username: 'u', + email: 'u@ex.com', + // 정확한 해시 문자열을 강제할 필요는 없음 + password: expect.any(String), + }); + expect(out).toEqual( + expect.objectContaining({ + id: 1, + username: 'u', + email: 'u@ex.com', + }) + ); + expect(out).not.toHaveProperty('password'); + }); + + test('닉네임 중복 → 409', async () => { + spyFindByUsername.mockResolvedValue({ id: 1 } as unknown as NonNullable< + Awaited> + >); + await expect( + userService.register('u', 'u@ex.com', '1234') + ).rejects.toThrow(AppError); + await expect( + userService.register('u', 'u@ex.com', '1234') + ).rejects.toMatchObject({ statusCode: 409 }); + }); + }); + + /* ───────── login ───────── */ + test('login → generateTokens 그대로 반환', async () => { + expect(jest.isMockFunction(token.generateTokens)).toBe(true); + mockGenerateTokens.mockReturnValue({ + accessToken: 'acc.token', + refreshToken: 'ref.token', + }); + + const out = await userService.login(7); + + expect(mockGenerateTokens).toHaveBeenCalledWith(7); + expect(out).toEqual({ + accessToken: 'acc.token', + refreshToken: 'ref.token', + }); + }); + + /* ───────── 프로필 조회/수정 ───────── */ + describe('get/update profile', () => { + test('getUserProfile', async () => { + type UserLite = Awaited< + ReturnType + >; + const userLite: UserLite = { id: 7, username: 'u' } as UserLite; + + spyFindById.mockResolvedValue(userLite); + + const out = await userService.getUserProfile(7); + expect(spyFindById).toHaveBeenCalledWith(7); + expect(out).toEqual({ id: 7, username: 'u' }); + }); + + test('updateUserProfile', async () => { + type UserUpdated = Awaited< + ReturnType + >; + const updated: UserUpdated = { + id: 7, + username: 'nu', + } as unknown as UserUpdated; + + spyUpdateUser.mockResolvedValue(updated); + + const out = await userService.updateUserProfile(7, { + username: 'nu', + email: 'e@x.com', + images: [], + }); + + expect(spyUpdateUser).toHaveBeenCalledWith(7, { + username: 'nu', + email: 'e@x.com', + images: [], + }); + expect(out).toEqual({ id: 7, username: 'nu' }); + }); + }); + + /* ───────── 비밀번호 변경 ───────── */ + describe('updatePassword', () => { + test('유저 없음 → 404', async () => { + spyFindByIdWithPassword.mockResolvedValue(null); + await expect( + userService.updatePassword(7, 'old', 'new') + ).rejects.toMatchObject({ statusCode: 404 }); + }); + + test('현재 비밀번호 불일치 → 400', async () => { + const stored = await bcrypt.hash('SOMETHING_ELSE', 10); + type WithPw = NonNullable< + Awaited> + >; + const withPw: WithPw = { id: 7, password: stored } as WithPw; + + spyFindByIdWithPassword.mockResolvedValue(withPw); + + await expect( + userService.updatePassword(7, 'wrong', 'new') + ).rejects.toMatchObject({ statusCode: 400 }); + }); + + test('새 비밀번호가 기존과 동일 → 400', async () => { + const same = await bcrypt.hash('old', 10); + type WithPw = NonNullable< + Awaited> + >; + const withPw: WithPw = { id: 7, password: same } as WithPw; + + spyFindByIdWithPassword.mockResolvedValue(withPw); + + await expect( + userService.updatePassword(7, 'old', 'old') + ).rejects.toMatchObject({ statusCode: 400 }); + }); + + test('성공 → 해시 후 업데이트 & password 제외', async () => { + const stored = await bcrypt.hash('old', 10); + type WithPw = NonNullable< + Awaited> + >; + const withPw: WithPw = { id: 7, password: stored } as WithPw; + spyFindByIdWithPassword.mockResolvedValue(withPw); + + // updatePassword가 리턴하는 유저 모양 + type Updated = Awaited< + ReturnType + >; + const updated: Updated = { + id: 7, + username: 'u', + email: 'u@ex.com', + password: await bcrypt.hash('new', 10), + images: [], + createdAt: new Date(), + updatedAt: new Date(), + } as Updated; + spyUpdatePassword.mockResolvedValue(updated); + + const out = await userService.updatePassword(7, 'old', 'new'); + + expect(spyUpdatePassword).toHaveBeenCalledWith(7, expect.any(String)); + expect(out).toEqual( + expect.objectContaining({ id: 7, username: 'u', email: 'u@ex.com' }) + ); + expect(out).not.toHaveProperty('password'); + }); + }); + + /* ───────── 댓글 집계 ───────── */ + describe('comments aggregation', () => { + test('getUserComments → likeCount 합성', async () => { + type Comments = Awaited< + ReturnType + >; + const comments: Comments = [ + { + id: 1, + content: 'a', + createdAt: new Date(), + article: null, + product: null, + }, + { + id: 2, + content: 'b', + createdAt: new Date(), + article: null, + product: null, + }, + ] as unknown as Comments; + + spyGetUserComments.mockResolvedValue(comments); + spyLikeCount.mockResolvedValueOnce(3).mockResolvedValueOnce(5); + + const out = await userService.getUserComments(7); + + expect(spyGetUserComments).toHaveBeenCalledWith(7); + expect(likeRepo.commentLikeRepository.count).toHaveBeenNthCalledWith( + 1, + 1 + ); + expect(likeRepo.commentLikeRepository.count).toHaveBeenNthCalledWith( + 2, + 2 + ); + + expect(out).toEqual([ + expect.objectContaining({ id: 1, likeCount: 3 }), + expect.objectContaining({ id: 2, likeCount: 5 }), + ]); + }); + + test('getUserLikedComments → 내부 comment 꺼내서 합성', async () => { + type Liked = Awaited< + ReturnType + >; + const liked: Liked = [ + { + comment: { + id: 10, + content: 'x', + createdAt: new Date(), + article: null, + product: null, + }, + }, + { + comment: { + id: 11, + content: 'y', + createdAt: new Date(), + article: null, + product: null, + }, + }, + ] as unknown as Liked; + + spyGetUserLikedComments.mockResolvedValue(liked); + spyLikeCount.mockResolvedValueOnce(2).mockResolvedValueOnce(0); + + const out = await userService.getUserLikedComments(7); + + expect(spyGetUserLikedComments).toHaveBeenCalledWith(7); + // 반환 스키마(예: { id, body, likeCount, isLiked })에 맞춰 검증 + expect(out).toEqual([ + expect.objectContaining({ id: 10, likeCount: 2, isLiked: true }), + expect.objectContaining({ id: 11, likeCount: 0, isLiked: true }), + ]); + }); + }); + + /* ───────── 토큰 쿠키 ───────── */ + describe('token cookies', () => { + test('setTokenCookies → 두 쿠키 설정', () => { + type ResForSet = Parameters[0]; + const resSet = { cookie: jest.fn() } as unknown as ResForSet; + + userService.setTokenCookies(resSet, 'A', 'R'); + + expect(resSet.cookie).toHaveBeenCalledTimes(2); + expect(resSet.cookie).toHaveBeenNthCalledWith( + 1, + ACCESS_TOKEN_COOKIE_NAME, + 'A', + expect.objectContaining({ + httpOnly: true, + sameSite: 'lax', + secure: false, + maxAge: expect.any(Number), + }) + ); + expect(resSet.cookie).toHaveBeenNthCalledWith( + 2, + REFRESH_TOKEN_COOKIE_NAME, + 'R', + expect.objectContaining({ + httpOnly: true, + sameSite: 'lax', + secure: false, + maxAge: expect.any(Number), + path: '/auth/refresh', + }) + ); + }); + + test('clearTokenCookies', () => { + type ResForClear = Parameters[0]; + const resClear = { clearCookie: jest.fn() } as unknown as ResForClear; + + userService.clearTokenCookies(resClear); + expect(resClear.clearCookie).toHaveBeenCalledWith( + ACCESS_TOKEN_COOKIE_NAME + ); + expect(resClear.clearCookie).toHaveBeenCalledWith( + REFRESH_TOKEN_COOKIE_NAME + ); + }); + + test('refreshTokens → verify→generate→setCookie→access 반환', async () => { + type ResForRefresh = Parameters[1]; + const res = { cookie: jest.fn() } as unknown as ResForRefresh; + + mockVerifyRefresh.mockReturnValue({ userId: 7 }); + mockGenerateTokens.mockReturnValue({ + accessToken: 'acc.token', + refreshToken: 'ref.token', + }); + + const access = await userService.refreshTokens('ref.token', res); + + expect(mockVerifyRefresh).toHaveBeenCalledWith('ref.token'); + expect(mockGenerateTokens).toHaveBeenCalledWith(7); + expect(res.cookie).toHaveBeenCalledTimes(2); + expect(access).toBe('acc.token'); + }); + }); +}); diff --git a/part4-mission11/tests/unit/unit.ws.test.ts b/part4-mission11/tests/unit/unit.ws.test.ts new file mode 100644 index 000000000..a5023d825 --- /dev/null +++ b/part4-mission11/tests/unit/unit.ws.test.ts @@ -0,0 +1,51 @@ +import { NotificationType as P } from '@prisma/client'; +import { Server } from 'socket.io'; + +import { + mapDomainToWire, + publishToUser, + __resetWsForTest, + __getUserSocketsForTest, +} from '../../src/lib/ws.js'; + +beforeEach(() => __resetWsForTest()); + +test('mapDomainToWire: PRICE_CHANGE → system', () => { + expect(mapDomainToWire(P.PRICE_CHANGE)).toBe('system'); +}); + +test('mapDomainToWire: NEW_COMMENT → chat', () => { + expect(mapDomainToWire(P.NEW_COMMENT)).toBe('chat'); +}); + +test('mapDomainToWire: default fallback', () => { + // @ts-expect-error 테스트용 가짜 값 + expect(mapDomainToWire('__UNKNOWN__')).toBe('system'); +}); + +test('publishToUser uses provided io', () => { + const io = new Server(); + const emitted: Array<{ event: string; payload: unknown }> = []; + + type ToReturn = ReturnType; + + const fakeBroadcast = { + emit: (event: string, payload?: unknown) => { + emitted.push({ event, payload }); + return true; + }, + } as unknown as ToReturn; + + const toSpy = jest.spyOn(io, 'to').mockReturnValue(fakeBroadcast); + + __getUserSocketsForTest().set(7, new Set(['sid1'])); + + publishToUser(io, { + userId: 7, + event: 'notification', + payload: { ok: true }, + }); + + expect(toSpy).toHaveBeenCalledWith('sid1'); + expect(emitted[0]).toEqual({ event: 'notification', payload: { ok: true } }); +}); diff --git a/part4-mission11/tests/unit/unit.wsAuth.test.ts b/part4-mission11/tests/unit/unit.wsAuth.test.ts new file mode 100644 index 000000000..3c03c9f65 --- /dev/null +++ b/part4-mission11/tests/unit/unit.wsAuth.test.ts @@ -0,0 +1,23 @@ +jest.mock('../../src/lib/token.js', () => ({ + __esModule: true, + verifyAccessToken: jest.fn(), +})); + +import { verifyAccessToken } from '../../src/lib/token.js'; +import { parseUserIdFromToken } from '../../src/lib/wsAuth.js'; + +test('parseUserIdFromToken → null (non-string)', () => { + expect(parseUserIdFromToken(undefined)).toBeNull(); +}); + +test('parseUserIdFromToken → null (verify throws)', () => { + (verifyAccessToken as jest.Mock).mockImplementation(() => { + throw new Error('bad'); + }); + expect(parseUserIdFromToken('X')).toBeNull(); +}); + +test('parseUserIdFromToken → number (ok)', () => { + const fakeVerify = (_t: string) => ({ userId: 42 }); + expect(parseUserIdFromToken('good', fakeVerify)).toBe(42); +}); diff --git a/part4-mission11/tsconfig.json b/part4-mission11/tsconfig.json new file mode 100644 index 000000000..990f27299 --- /dev/null +++ b/part4-mission11/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + // "baseUrl": "./", + "rootDir": "./src", + "outDir": "./dist", + "module": "nodenext", + "target": "esnext", + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "strict": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + "esModuleInterop": true, + "moduleResolution": "nodenext" + }, + "exclude": ["src/tests"], + "include": [ + "src", + "prisma/**/*", + "src/generated/prisma/**/*", + "jest.unit.config.cjs", + "src/types/**/*.d.ts" + ] +}