diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..6224fb2a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules +dist +.git +.github +coverage +temp +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.env +**/.env diff --git a/.env.sample b/.env.sample new file mode 100644 index 00000000..8f41e0b9 --- /dev/null +++ b/.env.sample @@ -0,0 +1,61 @@ +# TODO) ENV-Sample: 참고 용도로 건들 필요 없음 +# [필수] DATABASE_URL +# PostgreSQL 형식: postgresql://USER:PASSWORD@HOST:PORT/DATABASE +DATABASE_URL="postgresql://user:password@localhost:5432/<디비제목>" + +# 서버 포트 +PORT=3000 + +# 실행 환경 (development, production, test) +NODE_ENV=development + +# [선택] 디버그 (true/false) +DEBUG_MODE=true + +# [선택] 로그 레벨 (개발에서 디버그까지 보고 싶을 때) +LOG_LEVEL=debug + +# 베이스 URL (images) +BASE_URL=http://localhost:3000 + +# Socket.IO CORS 허용 도메인 목록 (쉼표로 구분) +# 예: http://localhost:5173,https://your-frontend.com +SOCKET_CORS_ORIGINS= + +### Session ### +# 세션 스토어 전용 (DB명 반드시 포함, ?schema 제거) +SESSION_STORE_URL="postgresql://user:password@localhost:5432/<디비제목>" + +# 세션 비밀키 (실제 서비스 시 랜덤 문자열 추천) +SESSION_SECRET= + +### JWT ### +# 해쉬 강도 (보통 10~12 추천) +SALT_ROUNDS= + +# 토큰 비밀키 (실제 서비스 시 랜덤 문자열 추천) +JWT_ACCESS_SECRET= +JWT_REFRESH_SECRET= + +# 토큰 유효기간 +# 분 단위 추천 예: 15m +JWT_ACCESS_EXPIRES= +# 일 단위 추천 예: 7d +JWT_REFRESH_EXPIRES= + +### Google OAuth 2.0 ### +# Google에서 발급한 Client ID +# OAuth 로그인 시작할 때 구글에 알려주는 "우리 앱의 ID" +GOOGLE_CLIENT_ID= + +# Google Client Secret +# Google에게 토큰 받기 위해 필요한 비밀키 (절대 공개 금지!) +GOOGLE_CLIENT_SECRET= + +### 추가 설명 ### +# 1) Google OAuth는 Redirect URI 반드시 일치해야 함 +# 예: http://localhost:3000/auth/google/callback +# Google Cloud Console → Authorized Redirect URIs에 등록 필수 +# +# 2) JWT Secret은 실제 서비스에서는 더 긴 랜덤 문자열 사용 +# 예: openssl rand -base64 32 diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000..5dbdffcc --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,47 @@ +name: CD + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Deploy to EC2 + uses: appleboy/ssh-action@v1.2.0 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + port: 22 + command_timeout: 30m + script: | + set -e + cd /home/ec2-user/sprint-5/6-sprint-mission-copy + export GIT_TERMINAL_PROMPT=0 + + # Prefer GitHub secret, fallback to EC2 .env. + if [ -n "${{ secrets.DATABASE_URL }}" ]; then + export DATABASE_URL='${{ secrets.DATABASE_URL }}' + fi + + if [ -z "${DATABASE_URL:-}" ] && [ -f .env ]; then + set -a + . ./.env + set +a + fi + + if [ -z "${DATABASE_URL:-}" ]; then + echo "DATABASE_URL is empty. Set GitHub secret DATABASE_URL or configure EC2 .env." + exit 1 + fi + + git pull --ff-only origin main + npm ci + npx prisma generate + npx prisma migrate deploy + npm run build + pm2 reload infra/ec2/ecosystem.config.cjs --update-env || pm2 start infra/ec2/ecosystem.config.cjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..c733b9a2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: CI + +on: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16 + ports: + - 5432:5432 + env: + POSTGRES_USER: ci_user + POSTGRES_PASSWORD: ci_password + POSTGRES_DB: ci_test_db + options: >- + --health-cmd "pg_isready -U ci_user -d ci_test_db" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + POSTGRES_USER: ci_user + POSTGRES_PASSWORD: ci_password + POSTGRES_DB: ci_test_db + DATABASE_URL: postgresql://ci_user:ci_password@localhost:5432/ci_test_db + JWT_ACCESS_SECRET: test-access-secret + JWT_REFRESH_SECRET: test-refresh-secret + DEBUG_MODE: "false" + LOG_LEVEL: error + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Generate Prisma Client + run: npx prisma generate + + - name: Apply migrations + run: npx prisma migrate deploy + + - name: Build + run: npm run build + + - name: Test + run: npm test diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a4440ec8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,158 @@ +# TODO) Git-Ignore: 노드 기반 프로젝트 공통 무시 규칙 +# ===================================================================== +# Node.js 개발환경에서 자동 생성되거나, 필요 없는 파일들 제외 +# 팀 개발 시 불필요한 파일이 Git에 올라가는 것을 방지하기 위한 설정 +# ===================================================================== + +### Node 기본 ### +# 로그 파일 (서버 실행 중 생기는 log들은 커밋할 필요 없음) +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Node 진단 리포트 (node --report-on-fatalerror) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# 실행 중 생성되는 pid 파일 +pids +*.pid +*.seed +*.pid.lock + +# jscoverage/JSCover 결과물 +lib-cov + +# 테스트 커버리지 디렉토리 +coverage +*.lcov + +# nyc 테스트 커버리지 출력 +.nyc_output + +# Grunt 내부 저장소 +.grunt + +# Bower 패키지 디렉토리 +bower_components + +# node-waf 설정 파일 +.lock-wscript + +# Node 애드온 빌드 파일 +build/Release + +# 의존성 패키지 디렉토리 +node_modules/ +jspm_packages/ + +# Snowpack 의존성 +web_modules/ + +# TypeScript 캐시 +*.tsbuildinfo + +# npm 캐시(선택) +.npm + +# ESLint 캐시 +.eslintcache + +# stylelint 캐시 +.stylelintcache + +# Microbundle 캐시 +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Node REPL 히스토리 +.node_repl_history + +# npm pack 생성 파일 +*.tgz + +# Yarn 무결성 파일 +.yarn-integrity + +### 환경 변수 ### +# 중요: 비밀키가 포함된 env 파일은 절대 Git에 올리지 않음 +.env +**/.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +### 웹 번들러 / 프레임워크 ### +# Parcel +.cache +.parcel-cache + +# Next.js 빌드 결과물 +.next +out + +# Nuxt.js 빌드 결과물 +.nuxt +dist + +# Gatsby 캐시 +.cache/ + +# VuePress 빌드 결과물 +.vuepress/dist +.temp + +# Docusaurus 캐시 +.docusaurus + +# Serverless 빌드 출력을 제외 +.serverless/ + +# FuseBox 캐시 +.fusebox/ + +# DynamoDB Local +.dynamodb/ + +# TernJS 포트 파일 +.tern-port + +### VSCode 관련 ### +# 개인 개발환경 설정 제외 (필요한 일부만 허용) +.vscode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# VSCode Local History +.history/ +*.vsix + +# Ionide (F# 관련) +.ionide + +### Yarn v2 ### +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### SvelteKit ### +.svelte-kit + +### Webpack ### +.webpack/ + +### 개인 작업 디렉토리 ### +# temp 폴더는 개인 테스트용이므로 Git에 올리지 않음 +temp/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..8a1c1ced --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5" +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..f9c72713 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY prisma ./prisma +RUN npx prisma generate + +COPY tsconfig.json ./ +COPY src ./src +COPY public ./public + +RUN npm run build + +RUN mkdir -p /app/public/uploads + +EXPOSE 3000 + +CMD ["sh", "-c", "npx prisma migrate deploy && node dist/app.js"] diff --git a/README.md b/README.md new file mode 100644 index 00000000..52b3f8b5 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +## 미션 목표 + +- 타입스크립트 마이그레이션하기 +- 타입스크립트 개발 환경 세팅하기 +- (심화) Layered Architecture 적용하기 + +## 기본 요구 사항 + +- 스프린트 미션 4의 구현이 완료된 상태에서 진행을 권장합니다. +- 타입스크립트 마이그레이션을 먼저 진행해 보고, 이전 미션에서 구현하지 못한 부분이 있다면 추가로 구현해 보세요. + +### 프로젝트 세팅 + +- tsconfig.json 파일을 생성하고, 필요한 옵션을 설정해 주세요. (예: outDir). +- 필요한 npm script를 설정해 주세요. (예: 빌드 및 개발 서버 실행 명령어) + +### 타입스크립트 마이그레이션 + +- 기존 Express.js 프로젝트를 타입스크립트 프로젝트로 마이그레이션 해주세요. +- 필요한 타입 패키지를 설치해 주세요. +- any 타입의 사용은 최소화해주세요. +- 복잡한 객체 구조나 배열 구조를 가진 변수에 인터페이스 또는 타입 별칭을 사용하세요. +- 필요한 경우, 타입 별칭 또는 유틸리티 타입을 사용해 타입 복잡성을 줄여주세요. +- 필요한 경우, declare를 사용하여 타입을 오버라이드하거나 확장합니다. (예: req.user) + +### 개발 환경 설정 + +- ts-node 를 사용해 .ts 코드를 바로 실행할 수 있는 npm script를 만들어 주세요. (예: npm run dev) +- nodemon을 사용해 .ts 코드가 변경될 때마다 서버가 다시 실행되는 npm script를 만들어 주세요. (예: npm run dev) + +## 심화 요구 사항 + +### Layered Architecture 적용하기 + +- Controller, Service, Repository로 나누어 코드를 리팩토링해 주세요. +- 필요하다면, 계층 사이에서 데이터를 주고 받을 때 DTO를 활용해 주세요. + +## 멘토님 리뷰 반영 + +src/middleware/auth.js + +```ts + const [type, token] = auth.split(' '); + + if (type !== 'Bearer' || !token) { + return res.status(401).json({ + +``` + +이 파일에서 처리하는 예외처리도 에러핸들러를 적용하면 좋을 것 같습니다 :) + +```ts +if (type !== 'Bearer' || !token) { + return next( + new UnauthorizedError( + 'authorization', + '승인 헤더가 없거나 형식이 잘못되었습니다' + ) + ); +} +``` + +--- + +src/controllers/user-controller.js + +```ts + const tokens = await authService.generateTokens(user); + + if (tokens.refreshToken) { +``` + +access token 세팅도 같이 부탁드립니다 :) + +```ts +if (tokens.accessToken) { + res.cookie('accessToken', tokens.accessToken, { + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + }); +} +``` + +--- + +src/controllers/user-controller.js + +```ts + nickname: user.nickname, + image: user.image, + }, + ...tokens, +``` + +토큰은 쿠키에만 세팅하고, 따로 응답값에 추가하지 않아도 됩니다~! + +```ts + nickname: user.nickname, + image: user.image, + }, +``` + +--- + +src/controllers/upload-controller.js + +```ts + // ?) 이미지 업로드 응답 + image(req, res) { + if (!req.file) { + return res.status(400).json({ +``` + +이부분도 만드신 에러핸들러 사용해주세요 :) + +```ts +if (!req.file) { + return next(new ValidationError('file', '업로드된 파일이 없습니다')); +} +``` diff --git a/algorithm/BinarySearchTree.js b/algorithm/BinarySearchTree.js new file mode 100644 index 00000000..85c85dec --- /dev/null +++ b/algorithm/BinarySearchTree.js @@ -0,0 +1,111 @@ +class BinarySearchTreeNode { + constructor(value) { + this.value = value; + this.left = null; + this.right = null; + } +} + +export class BinarySearchTree { + constructor() { + this.root = null; + } + + insert(value) { + const newNode = new BinarySearchTreeNode(value); + + if (this.root === null) { + this.root = newNode; + return newNode; + } + + let currentNode = this.root; + + while (true) { + if (value < currentNode.value) { + if (currentNode.left === null) { + currentNode.left = newNode; + return newNode; + } + + currentNode = currentNode.left; + } else { + if (currentNode.right === null) { + currentNode.right = newNode; + return newNode; + } + + currentNode = currentNode.right; + } + } + } + + find(value) { + let currentNode = this.root; + + while (currentNode !== null) { + if (value === currentNode.value) { + return currentNode; + } + + if (value < currentNode.value) { + currentNode = currentNode.left; + } else { + currentNode = currentNode.right; + } + } + + return null; + } + + remove(value) { + let removedNode = null; + + const removeRecursive = (node, targetValue) => { + if (node === null) { + return null; + } + + if (targetValue < node.value) { + node.left = removeRecursive(node.left, targetValue); + return node; + } + + if (targetValue > node.value) { + node.right = removeRecursive(node.right, targetValue); + return node; + } + + removedNode = new BinarySearchTreeNode(node.value); + removedNode.left = node.left; + removedNode.right = node.right; + + if (node.left === null) { + return node.right; + } + + if (node.right === null) { + return node.left; + } + + let successorParent = node; + let successorNode = node.right; + + while (successorNode.left !== null) { + successorParent = successorNode; + successorNode = successorNode.left; + } + + if (successorParent !== node) { + successorParent.left = successorNode.right; + successorNode.right = node.right; + } + + successorNode.left = node.left; + return successorNode; + }; + + this.root = removeRecursive(this.root, value); + return removedNode; + } +} diff --git a/algorithm/DoublyLinkedList.js b/algorithm/DoublyLinkedList.js new file mode 100644 index 00000000..90f601fa --- /dev/null +++ b/algorithm/DoublyLinkedList.js @@ -0,0 +1,108 @@ +class DoublyLinkedListNode { + constructor(value) { + this.value = value; + this.prev = null; + this.next = null; + } +} + +export class DoublyLinkedList { + constructor() { + this.head = null; + this.tail = null; + } + + addToHead(value) { + const newNode = new DoublyLinkedListNode(value); + + if (this.head === null) { + this.head = newNode; + this.tail = newNode; + return newNode; + } + + newNode.next = this.head; + this.head.prev = newNode; + this.head = newNode; + + return newNode; + } + + addToTail(value) { + const newNode = new DoublyLinkedListNode(value); + + if (this.tail === null) { + this.head = newNode; + this.tail = newNode; + return newNode; + } + + newNode.prev = this.tail; + this.tail.next = newNode; + this.tail = newNode; + + return newNode; + } + + insertAfter(targetValue, newValue) { + const targetNode = this.findNode(targetValue); + + if (targetNode === null) { + return null; + } + + const newNode = new DoublyLinkedListNode(newValue); + const nextNode = targetNode.next; + + newNode.prev = targetNode; + newNode.next = nextNode; + targetNode.next = newNode; + + if (nextNode !== null) { + nextNode.prev = newNode; + } else { + this.tail = newNode; + } + + return newNode; + } + + findNode(value) { + let currentNode = this.head; + + while (currentNode !== null) { + if (currentNode.value === value) { + return currentNode; + } + + currentNode = currentNode.next; + } + + return null; + } + + removeNode(value) { + const targetNode = this.findNode(value); + + if (targetNode === null) { + return null; + } + + if (targetNode.prev !== null) { + targetNode.prev.next = targetNode.next; + } else { + this.head = targetNode.next; + } + + if (targetNode.next !== null) { + targetNode.next.prev = targetNode.prev; + } else { + this.tail = targetNode.prev; + } + + targetNode.prev = null; + targetNode.next = null; + + return targetNode; + } +} diff --git a/algorithm/LinkedList.js b/algorithm/LinkedList.js new file mode 100644 index 00000000..df86c98f --- /dev/null +++ b/algorithm/LinkedList.js @@ -0,0 +1,72 @@ +class LinkedListNode { + constructor(value) { + this.value = value; + this.next = null; + } +} + +export class LinkedList { + constructor() { + this.head = null; + } + + addNode(value) { + const newNode = new LinkedListNode(value); + + if (this.head === null) { + this.head = newNode; + return newNode; + } + + let currentNode = this.head; + + while (currentNode.next !== null) { + currentNode = currentNode.next; + } + + currentNode.next = newNode; + return newNode; + } + + findNode(value) { + let currentNode = this.head; + + while (currentNode !== null) { + if (currentNode.value === value) { + return currentNode; + } + + currentNode = currentNode.next; + } + + return null; + } + + insertAfter(targetValue, newValue) { + const targetNode = this.findNode(targetValue); + + if (targetNode === null) { + return null; + } + + const newNode = new LinkedListNode(newValue); + newNode.next = targetNode.next; + targetNode.next = newNode; + + return newNode; + } + + removeAfter(targetValue) { + const targetNode = this.findNode(targetValue); + + if (targetNode === null || targetNode.next === null) { + return null; + } + + const removedNode = targetNode.next; + targetNode.next = removedNode.next; + removedNode.next = null; + + return removedNode; + } +} diff --git a/algorithm/Queue.js b/algorithm/Queue.js new file mode 100644 index 00000000..3a5b90b5 --- /dev/null +++ b/algorithm/Queue.js @@ -0,0 +1,29 @@ +export class Queue { + constructor() { + this.items = []; + } + + enqueue(value) { + this.items.push(value); + } + + dequeue() { + if (this.isEmpty()) { + return null; + } + + return this.items.shift(); + } + + peek() { + if (this.isEmpty()) { + return null; + } + + return this.items[0]; + } + + isEmpty() { + return this.items.length === 0; + } +} diff --git a/algorithm/Stack.js b/algorithm/Stack.js new file mode 100644 index 00000000..0841c1aa --- /dev/null +++ b/algorithm/Stack.js @@ -0,0 +1,29 @@ +export class Stack { + constructor() { + this.items = []; + } + + push(value) { + this.items.push(value); + } + + pop() { + if (this.isEmpty()) { + return null; + } + + return this.items.pop(); + } + + peek() { + if (this.isEmpty()) { + return null; + } + + return this.items[this.items.length - 1]; + } + + isEmpty() { + return this.items.length === 0; + } +} diff --git a/algorithm/sorts.js b/algorithm/sorts.js new file mode 100644 index 00000000..80a256b3 --- /dev/null +++ b/algorithm/sorts.js @@ -0,0 +1,131 @@ +function swap(nums, leftIndex, rightIndex) { + [nums[leftIndex], nums[rightIndex]] = [nums[rightIndex], nums[leftIndex]]; +} + +export function selectionSort(nums) { + for (let i = 0; i < nums.length - 1; i += 1) { + let minIndex = i; + + for (let j = i + 1; j < nums.length; j += 1) { + if (nums[j] < nums[minIndex]) { + minIndex = j; + } + } + + if (minIndex !== i) { + swap(nums, i, minIndex); + } + } +} + +export function insertionSort(nums) { + for (let i = 1; i < nums.length; i += 1) { + const current = nums[i]; + let j = i - 1; + + while (j >= 0 && nums[j] > current) { + nums[j + 1] = nums[j]; + j -= 1; + } + + nums[j + 1] = current; + } +} + +function merge(left, right) { + const merged = []; + let leftIndex = 0; + let rightIndex = 0; + + while (leftIndex < left.length && rightIndex < right.length) { + if (left[leftIndex] <= right[rightIndex]) { + merged.push(left[leftIndex]); + leftIndex += 1; + } else { + merged.push(right[rightIndex]); + rightIndex += 1; + } + } + + return merged + .concat(left.slice(leftIndex)) + .concat(right.slice(rightIndex)); +} + +export function mergeSort(nums) { + if (nums.length <= 1) { + return [...nums]; + } + + const middle = Math.floor(nums.length / 2); + const left = mergeSort(nums.slice(0, middle)); + const right = mergeSort(nums.slice(middle)); + + return merge(left, right); +} + +function partition(nums, low, high) { + const pivot = nums[high]; + let smallerIndex = low; + + for (let i = low; i < high; i += 1) { + if (nums[i] <= pivot) { + swap(nums, i, smallerIndex); + smallerIndex += 1; + } + } + + swap(nums, smallerIndex, high); + return smallerIndex; +} + +function quickSortRecursive(nums, low, high) { + if (low >= high) { + return; + } + + const pivotIndex = partition(nums, low, high); + + quickSortRecursive(nums, low, pivotIndex - 1); + quickSortRecursive(nums, pivotIndex + 1, high); +} + +export function quickSort(nums) { + quickSortRecursive(nums, 0, nums.length - 1); +} + +function heapify(nums, heapSize, rootIndex) { + let largestIndex = rootIndex; + const leftChildIndex = rootIndex * 2 + 1; + const rightChildIndex = rootIndex * 2 + 2; + + if ( + leftChildIndex < heapSize && + nums[leftChildIndex] > nums[largestIndex] + ) { + largestIndex = leftChildIndex; + } + + if ( + rightChildIndex < heapSize && + nums[rightChildIndex] > nums[largestIndex] + ) { + largestIndex = rightChildIndex; + } + + if (largestIndex !== rootIndex) { + swap(nums, rootIndex, largestIndex); + heapify(nums, heapSize, largestIndex); + } +} + +export function heapsort(nums) { + for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i -= 1) { + heapify(nums, nums.length, i); + } + + for (let i = nums.length - 1; i > 0; i -= 1) { + swap(nums, 0, i); + heapify(nums, i, 0); + } +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..fda4883f --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,44 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: sprint11-app + depends_on: + postgres: + condition: service_healthy + environment: + PORT: 3000 + NODE_ENV: production + DATABASE_URL: postgresql://sprint11:sprint11@postgres:5432/sprint11 + JWT_ACCESS_SECRET: dev-access-secret + JWT_REFRESH_SECRET: dev-refresh-secret + DEBUG_MODE: "false" + LOG_LEVEL: info + ports: + - "3000:3000" + volumes: + - uploads_data:/app/public/uploads + restart: unless-stopped + + postgres: + image: postgres:16-alpine + container_name: sprint11-postgres + environment: + POSTGRES_USER: sprint11 + POSTGRES_PASSWORD: sprint11 + POSTGRES_DB: sprint11 + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + +volumes: + postgres_data: + uploads_data: diff --git a/infra/ec2/ecosystem.config.cjs b/infra/ec2/ecosystem.config.cjs new file mode 100644 index 00000000..b92bd02c --- /dev/null +++ b/infra/ec2/ecosystem.config.cjs @@ -0,0 +1,11 @@ +module.exports = { + apps: [ + { + script: 'npm', + args: 'start', + env: { + NODE_ENV: 'production', + }, + }, + ], +}; diff --git a/infra/ec2/nginx.conf b/infra/ec2/nginx.conf new file mode 100644 index 00000000..c27914a9 --- /dev/null +++ b/infra/ec2/nginx.conf @@ -0,0 +1,15 @@ +server { + listen 80; + server_name _; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/infra/ec2/secure-group-inbound.png b/infra/ec2/secure-group-inbound.png new file mode 100644 index 00000000..8633de70 Binary files /dev/null and b/infra/ec2/secure-group-inbound.png differ diff --git a/infra/ec2/secure-group-outbound.png b/infra/ec2/secure-group-outbound.png new file mode 100644 index 00000000..8b228bb0 Binary files /dev/null and b/infra/ec2/secure-group-outbound.png differ diff --git a/infra/ec2/start.sh b/infra/ec2/start.sh new file mode 100644 index 00000000..9966ef24 --- /dev/null +++ b/infra/ec2/start.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +npm install +npx prisma generate +npx prisma migrate deploy +npm run build + +pm2 start ecosystem.config.cjs \ No newline at end of file diff --git a/infra/rds/secure-group-inbound.png b/infra/rds/secure-group-inbound.png new file mode 100644 index 00000000..48d28442 Binary files /dev/null and b/infra/rds/secure-group-inbound.png differ diff --git a/infra/rds/secure-group-outbound.png b/infra/rds/secure-group-outbound.png new file mode 100644 index 00000000..2e2036dd Binary files /dev/null and b/infra/rds/secure-group-outbound.png differ diff --git a/infra/s3/policy.png b/infra/s3/policy.png new file mode 100644 index 00000000..8002c85e Binary files /dev/null and b/infra/s3/policy.png differ diff --git a/jest.config.cjs b/jest.config.cjs new file mode 100644 index 00000000..297a4b2e --- /dev/null +++ b/jest.config.cjs @@ -0,0 +1,31 @@ +module.exports = { + preset: 'ts-jest/presets/default-esm', + testEnvironment: 'node', + extensionsToTreatAsEsm: ['.ts'], + transform: { + '^.+\\.ts$': [ + 'ts-jest', + { + useESM: true, + tsconfig: 'tsconfig.json', + diagnostics: { + ignoreCodes: [151002], + }, + }, + ], + }, + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + testMatch: ['/src/__tests__/**/*.test.ts'], + setupFiles: ['/src/test/jest.setup.cjs'], + collectCoverage: true, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/types/**', + '!src/**/test/**', + '!src/test/**', + '!src/**/config/**', + ], + coverageDirectory: 'coverage', +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..2c36f565 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7124 @@ +{ + "name": "nb6-mission-sprint-5", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nb6-mission-sprint-5", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@prisma/client": "^6.19.0", + "axios": "^1.13.2", + "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "dotenv": "^16.6.1", + "express": "^4.21.2", + "express-jwt": "^8.5.1", + "express-session": "^1.18.2", + "jsonwebtoken": "^9.0.2", + "multer": "^2.0.2", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3", + "superstruct": "^2.0.2", + "winston": "^3.18.3", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/cookie-parser": "^1.4.8", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/express-jwt": "^7.4.4", + "@types/express-session": "^1.18.1", + "@types/jest": "^29.5.14", + "@types/jsonwebtoken": "^9.0.7", + "@types/multer": "^1.4.12", + "@types/node": "^22.19.1", + "@types/passport": "^1.0.16", + "@types/passport-jwt": "^4.0.1", + "@types/socket.io": "^3.0.1", + "@types/socket.io-client": "^1.4.36", + "@types/supertest": "^6.0.3", + "jest": "^29.7.0", + "nodemon": "^3.1.0", + "prisma": "^6.19.0", + "supertest": "^7.2.2", + "ts-jest": "^29.4.6", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/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==", + "dev": true, + "license": "MIT" + }, + "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.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/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/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "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.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "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.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "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.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "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.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "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/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "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/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "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/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/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/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/reporters/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/@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/source-map/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/@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/transform/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/@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/gen-mapping/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/@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/remapping/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/@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.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/@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/@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.19.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.0.tgz", + "integrity": "sha512-QXFT+N/bva/QI2qoXmjBzL7D6aliPffIwP+81AdTGq0FXDoLxLkWivGMawG8iM5B9BKfxLIXxfWWAF6wbuJU6g==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.0.tgz", + "integrity": "sha512-zwCayme+NzI/WfrvFEtkFhhOaZb/hI+X8TTjzjJ252VbPxAl2hWHK5NMczmnG9sXck2lsXrxIZuK524E25UNmg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.0.tgz", + "integrity": "sha512-8hAdGG7JmxrzFcTzXZajlQCidX0XNkMJkpqtfbLV54wC6LSSX6Vni25W/G+nAANwLnZ2TmwkfIuWetA7jJxJFA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.0.tgz", + "integrity": "sha512-pMRJ+1S6NVdXoB8QJAPIGpKZevFjxhKt0paCkRDTZiczKb7F4yTgRP8M4JdVkpQwmaD4EoJf6qA+p61godDokw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.0", + "@prisma/engines-version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", + "@prisma/fetch-engine": "6.19.0", + "@prisma/get-platform": "6.19.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773.tgz", + "integrity": "sha512-gV7uOBQfAFlWDvPJdQxMT1aSRur3a0EkU/6cfbAC5isV67tKDWUrPauyaHNpB+wN1ebM4A9jn/f4gH+3iHSYSQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.0.tgz", + "integrity": "sha512-OOx2Lda0DGrZ1rodADT06ZGqHzr7HY7LNMaFE2Vp8dp146uJld58sRuasdX0OiwpHgl8SqDTUKHNUyzEq7pDdQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.0", + "@prisma/engines-version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", + "@prisma/get-platform": "6.19.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.0.tgz", + "integrity": "sha512-ym85WDO2yDhC3fIXHWYpG3kVMBA49cL1XD2GCsCF8xbwoy2OkDQY44gEbAt2X46IQ4Apq9H6g0Ex1iFfPqEkHA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.0" + } + }, + "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/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "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/@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": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-jwt": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-7.4.4.tgz", + "integrity": "sha512-F0f+zvdAT6gBOtMf0XTkNTvwiqXv0JjW0yj6nZM1Q03P8cCoA0+zrg+JeFv+AyaJ50qJJadBpbqOSdr4kYjuyg==", + "deprecated": "This is a stub types definition. express-jwt provides its own type definitions, so you do not need this installed.", + "dev": true, + "license": "MIT", + "dependencies": { + "express-jwt": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "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, + "license": "MIT" + }, + "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.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "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==", + "license": "MIT", + "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, + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", + "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz", + "integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/socket.io": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", + "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "socket.io": "*" + } + }, + "node_modules/@types/socket.io-client": { + "version": "1.4.36", + "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.36.tgz", + "integrity": "sha512-ZJWjtFBeBy1kRSYpVbeGYTElf6BqPQUkXDlHHD4k/42byCN5Rh027f4yARHCink9sKAkbtGZXEAmR0ZCnc2/Ag==", + "dev": true, + "license": "MIT" + }, + "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/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "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/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/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-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/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/ansi-styles/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/ansi-styles/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/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "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": "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/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/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==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "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==", + "dev": true, + "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.9.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz", + "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==", + "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, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/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-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/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, + "license": "MIT", + "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": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "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, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "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==", + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "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==", + "license": "MIT", + "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==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.279", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz", + "integrity": "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==", + "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, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "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.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "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-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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==", + "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/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==", + "license": "MIT" + }, + "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/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/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/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": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-jwt": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-8.5.1.tgz", + "integrity": "sha512-Dv6QjDLpR2jmdb8M6XQXiCcpEom7mK8TOqnr0/TngDKsG2DHVkO8+XnVxkJVN7BuS1I3OrGw6N8j5DaaGgkDRQ==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9", + "express-unless": "^2.1.3", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/express-unless": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz", + "integrity": "sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ==", + "license": "MIT" + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "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" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.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-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/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/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "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/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-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==", + "license": "MIT", + "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/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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==", + "license": "MIT", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "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.", + "dev": true, + "license": "ISC", + "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==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-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-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-lib-source-maps/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/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==", + "dev": true, + "license": "MIT" + }, + "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/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.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "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": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "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-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/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/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "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/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "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/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.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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/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/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==", + "dev": true, + "license": "ISC", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "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": "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/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "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, + "license": "MIT" + }, + "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==", + "license": "MIT", + "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.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "license": "MIT", + "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/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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.2", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", + "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.3.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==", + "license": "MIT", + "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==", + "license": "MIT", + "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, + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "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/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/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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.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==", + "dev": true, + "license": "MIT", + "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": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "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==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "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.19.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.0.tgz", + "integrity": "sha512-F3eX7K+tWpkbhl3l4+VkFtrwJlLXbAM+f9jolgoUZbFcm1DgHZ4cq9AgVEgUym2au5Ad/TDLN8lg83D+M10ycw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.19.0", + "@prisma/engines": "6.19.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==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "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" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "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.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/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" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/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": "0.19.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", + "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/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==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/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==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "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.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "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.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "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/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/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "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/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/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/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/superagent/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.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==", + "dev": true, + "license": "MIT", + "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/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/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "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 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.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 + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT" + }, + "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/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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/v8-to-istanbul/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/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "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/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "ISC" + }, + "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.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "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==", + "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==", + "license": "MIT", + "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/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/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..5f976fe5 --- /dev/null +++ b/package.json @@ -0,0 +1,71 @@ +{ + "name": "nb6-mission-sprint-5", + "version": "1.0.0", + "description": "Convert sprint-4 to TypeScript", + "license": "ISC", + "author": "leon", + "type": "module", + "ts-node": { + "esm": true, + "experimentalResolver": true, + "experimentalSpecifierResolution": "node" + }, + "main": "dist/app.js", + "scripts": { + "dev": "nodemon --watch src --ext ts --exec \"npm run build && node dist/app.js\"", + "build": "tsc -p tsconfig.json", + "start": "node dist/app.js", + "prestart": "npm run build", + "seed": "node prisma/seed.js", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:studio": "prisma studio", + "prisma:deploy": "prisma migrate deploy", + "test": "NODE_ENV=test node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand", + "test:coverage": "NODE_ENV=test node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --coverage" + }, + "dependencies": { + "@prisma/client": "^6.19.0", + "axios": "^1.13.2", + "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "dotenv": "^16.6.1", + "express": "^4.21.2", + "express-jwt": "^8.5.1", + "express-session": "^1.18.2", + "jsonwebtoken": "^9.0.2", + "multer": "^2.0.2", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3", + "superstruct": "^2.0.2", + "winston": "^3.18.3", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/cookie-parser": "^1.4.8", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/express-jwt": "^7.4.4", + "@types/express-session": "^1.18.1", + "@types/jest": "^29.5.14", + "@types/jsonwebtoken": "^9.0.7", + "@types/multer": "^1.4.12", + "@types/node": "^22.19.1", + "@types/passport": "^1.0.16", + "@types/passport-jwt": "^4.0.1", + "@types/socket.io": "^3.0.1", + "@types/socket.io-client": "^1.4.36", + "@types/supertest": "^6.0.3", + "jest": "^29.7.0", + "nodemon": "^3.1.0", + "prisma": "^6.19.0", + "supertest": "^7.2.2", + "ts-jest": "^29.4.6", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } +} diff --git a/prisma/migrations/20251021094220_init/migration.sql b/prisma/migrations/20251021094220_init/migration.sql new file mode 100644 index 00000000..67d959c6 --- /dev/null +++ b/prisma/migrations/20251021094220_init/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "address" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/prisma/migrations/20251021095306_add_user_age/migration.sql b/prisma/migrations/20251021095306_add_user_age/migration.sql new file mode 100644 index 00000000..bb4d28f4 --- /dev/null +++ b/prisma/migrations/20251021095306_add_user_age/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "age" INTEGER; diff --git a/prisma/migrations/20251021095536_alter_user_age/migration.sql b/prisma/migrations/20251021095536_alter_user_age/migration.sql new file mode 100644 index 00000000..5967159b --- /dev/null +++ b/prisma/migrations/20251021095536_alter_user_age/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Made the column `age` on table `User` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "age" SET NOT NULL; diff --git a/prisma/migrations/20251021095631_delete_user_age/migration.sql b/prisma/migrations/20251021095631_delete_user_age/migration.sql new file mode 100644 index 00000000..0737346c --- /dev/null +++ b/prisma/migrations/20251021095631_delete_user_age/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `age` on the `User` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "User" DROP COLUMN "age"; diff --git a/prisma/migrations/20251022063044_add_product/migration.sql b/prisma/migrations/20251022063044_add_product/migration.sql new file mode 100644 index 00000000..f4d2aaaa --- /dev/null +++ b/prisma/migrations/20251022063044_add_product/migration.sql @@ -0,0 +1,16 @@ +-- CreateEnum +CREATE TYPE "Category" AS ENUM ('FASHION', 'BEAUTY', 'SPORTS', 'ELECTRONICS', 'HOME_INTERIOR'); + +-- CreateTable +CREATE TABLE "Product" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "category" "Category" NOT NULL, + "price" DOUBLE PRECISION NOT NULL, + "stock" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Product_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/migrations/20251022092556_add_user_oders/migration.sql b/prisma/migrations/20251022092556_add_user_oders/migration.sql new file mode 100644 index 00000000..cb3fe409 --- /dev/null +++ b/prisma/migrations/20251022092556_add_user_oders/migration.sql @@ -0,0 +1,33 @@ +-- CreateEnum +CREATE TYPE "OrderStatus" AS ENUM ('PENDING', 'COMPLETE'); + +-- CreateTable +CREATE TABLE "UserPreference" ( + "id" TEXT NOT NULL, + "receiveEmail" BOOLEAN NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "UserPreference_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Order" ( + "id" TEXT NOT NULL, + "status" "OrderStatus" NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Order_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OrderItem" ( + "id" TEXT NOT NULL, + "unitPrice" DOUBLE PRECISION NOT NULL, + "quantity" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "OrderItem_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/migrations/20251022093044_alter_user_order/migration.sql b/prisma/migrations/20251022093044_alter_user_order/migration.sql new file mode 100644 index 00000000..185ca596 --- /dev/null +++ b/prisma/migrations/20251022093044_alter_user_order/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - Added the required column `userId` to the `Order` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Order" ADD COLUMN "userId" TEXT NOT NULL; + +-- AddForeignKey +ALTER TABLE "Order" ADD CONSTRAINT "Order_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20251022094729_add_user_preference/migration.sql b/prisma/migrations/20251022094729_add_user_preference/migration.sql new file mode 100644 index 00000000..c18070aa --- /dev/null +++ b/prisma/migrations/20251022094729_add_user_preference/migration.sql @@ -0,0 +1,39 @@ +/* + Warnings: + + - A unique constraint covering the columns `[userId]` on the table `UserPreference` will be added. If there are existing duplicate values, this will fail. + - Added the required column `userId` to the `UserPreference` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "OrderItem" ADD COLUMN "productId" TEXT; + +-- AlterTable +ALTER TABLE "UserPreference" ADD COLUMN "userId" TEXT NOT NULL; + +-- CreateTable +CREATE TABLE "_ProductToUser" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_ProductToUser_AB_unique" ON "_ProductToUser"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ProductToUser_B_index" ON "_ProductToUser"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserPreference_userId_key" ON "UserPreference"("userId"); + +-- AddForeignKey +ALTER TABLE "UserPreference" ADD CONSTRAINT "UserPreference_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ProductToUser" ADD CONSTRAINT "_ProductToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ProductToUser" ADD CONSTRAINT "_ProductToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20251022100335_add_user_ondelete/migration.sql b/prisma/migrations/20251022100335_add_user_ondelete/migration.sql new file mode 100644 index 00000000..6b370605 --- /dev/null +++ b/prisma/migrations/20251022100335_add_user_ondelete/migration.sql @@ -0,0 +1,34 @@ +/* + Warnings: + + - Added the required column `orderId` to the `OrderItem` table without a default value. This is not possible if the table is not empty. + - Made the column `productId` on table `OrderItem` required. This step will fail if there are existing NULL values in that column. + +*/ +-- DropForeignKey +ALTER TABLE "Order" DROP CONSTRAINT "Order_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "OrderItem" DROP CONSTRAINT "OrderItem_productId_fkey"; + +-- DropForeignKey +ALTER TABLE "UserPreference" DROP CONSTRAINT "UserPreference_userId_fkey"; + +-- AlterTable +ALTER TABLE "Order" ALTER COLUMN "userId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "OrderItem" ADD COLUMN "orderId" TEXT NOT NULL, +ALTER COLUMN "productId" SET NOT NULL; + +-- AddForeignKey +ALTER TABLE "UserPreference" ADD CONSTRAINT "UserPreference_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Order" ADD CONSTRAINT "Order_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20251022124851_add_new_schema_category/migration.sql b/prisma/migrations/20251022124851_add_new_schema_category/migration.sql new file mode 100644 index 00000000..fe004f6d --- /dev/null +++ b/prisma/migrations/20251022124851_add_new_schema_category/migration.sql @@ -0,0 +1,10 @@ +-- AlterEnum +-- This migration adds more than one value to an enum. +-- With PostgreSQL versions 11 and earlier, this is not possible +-- in a single migration. This can be worked around by creating +-- multiple migrations, each migration adding only one value to +-- the enum. + + +ALTER TYPE "Category" ADD VALUE 'HOUSEHOLD_SUPPLIES'; +ALTER TYPE "Category" ADD VALUE 'KITCHENWARE'; diff --git a/prisma/migrations/20251026091727_new_schema_mission/migration.sql b/prisma/migrations/20251026091727_new_schema_mission/migration.sql new file mode 100644 index 00000000..722f7c65 --- /dev/null +++ b/prisma/migrations/20251026091727_new_schema_mission/migration.sql @@ -0,0 +1,73 @@ +/* + Warnings: + + - The primary key for the `Product` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `category` on the `Product` table. All the data in the column will be lost. + - The `id` column on the `Product` table would be dropped and recreated. This will lead to data loss if there is data in the column. + - You are about to drop the `Order` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `OrderItem` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `UserPreference` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `_ProductToUser` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- CreateEnum +CREATE TYPE "Tag" AS ENUM ('NONE', 'FASHION', 'BEAUTY', 'SPORTS', 'ELECTRONICS', 'HOME_INTERIOR', 'HOUSEHOLD_SUPPLIES', 'KITCHENWARE'); + +-- DropForeignKey +ALTER TABLE "Order" DROP CONSTRAINT "Order_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "OrderItem" DROP CONSTRAINT "OrderItem_orderId_fkey"; + +-- DropForeignKey +ALTER TABLE "OrderItem" DROP CONSTRAINT "OrderItem_productId_fkey"; + +-- DropForeignKey +ALTER TABLE "UserPreference" DROP CONSTRAINT "UserPreference_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "_ProductToUser" DROP CONSTRAINT "_ProductToUser_A_fkey"; + +-- DropForeignKey +ALTER TABLE "_ProductToUser" DROP CONSTRAINT "_ProductToUser_B_fkey"; + +-- AlterTable +ALTER TABLE "Product" DROP CONSTRAINT "Product_pkey", +DROP COLUMN "category", +ADD COLUMN "tags" "Tag" NOT NULL DEFAULT 'NONE', +DROP COLUMN "id", +ADD COLUMN "id" SERIAL NOT NULL, +ADD CONSTRAINT "Product_pkey" PRIMARY KEY ("id"); + +-- DropTable +DROP TABLE "Order"; + +-- DropTable +DROP TABLE "OrderItem"; + +-- DropTable +DROP TABLE "User"; + +-- DropTable +DROP TABLE "UserPreference"; + +-- DropTable +DROP TABLE "_ProductToUser"; + +-- DropEnum +DROP TYPE "Category"; + +-- DropEnum +DROP TYPE "OrderStatus"; + +-- CreateTable +CREATE TABLE "Article" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Article_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/migrations/20251026235500_fix_schema_model/migration.sql b/prisma/migrations/20251026235500_fix_schema_model/migration.sql new file mode 100644 index 00000000..5a90423b --- /dev/null +++ b/prisma/migrations/20251026235500_fix_schema_model/migration.sql @@ -0,0 +1,40 @@ +-- CreateTable +CREATE TABLE "Comment" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "articleId" INTEGER NOT NULL, + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProductComment" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "productId" INTEGER NOT NULL, + + CONSTRAINT "ProductComment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Purchase" ( + "id" SERIAL NOT NULL, + "quantity" INTEGER NOT NULL, + "unitPrice" DOUBLE PRECISION NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "productId" INTEGER NOT NULL, + + CONSTRAINT "Purchase_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProductComment" ADD CONSTRAINT "ProductComment_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20251027070427_add_schema_image/migration.sql b/prisma/migrations/20251027070427_add_schema_image/migration.sql new file mode 100644 index 00000000..451728db --- /dev/null +++ b/prisma/migrations/20251027070427_add_schema_image/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Product" ADD COLUMN "imagePath" TEXT; diff --git a/prisma/migrations/20251027142504_add_schema_product_name/migration.sql b/prisma/migrations/20251027142504_add_schema_product_name/migration.sql new file mode 100644 index 00000000..e34dc571 --- /dev/null +++ b/prisma/migrations/20251027142504_add_schema_product_name/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Purchase" ADD COLUMN "productName" TEXT; diff --git a/prisma/migrations/20251027142807_delete_schema_produc_name/migration.sql b/prisma/migrations/20251027142807_delete_schema_produc_name/migration.sql new file mode 100644 index 00000000..89024b80 --- /dev/null +++ b/prisma/migrations/20251027142807_delete_schema_produc_name/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `productName` on the `Purchase` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Purchase" DROP COLUMN "productName"; diff --git a/prisma/migrations/20251104134150_fix_schema_article_comment/migration.sql b/prisma/migrations/20251104134150_fix_schema_article_comment/migration.sql new file mode 100644 index 00000000..f223bfa2 --- /dev/null +++ b/prisma/migrations/20251104134150_fix_schema_article_comment/migration.sql @@ -0,0 +1,24 @@ +/* + Warnings: + + - You are about to drop the `Comment` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "Comment" DROP CONSTRAINT "Comment_articleId_fkey"; + +-- DropTable +DROP TABLE "Comment"; + +-- CreateTable +CREATE TABLE "ArticleComment" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "articleId" INTEGER NOT NULL, + + CONSTRAINT "ArticleComment_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "ArticleComment" ADD CONSTRAINT "ArticleComment_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260116023636_new_model_noti/migration.sql b/prisma/migrations/20260116023636_new_model_noti/migration.sql new file mode 100644 index 00000000..6ab78a02 --- /dev/null +++ b/prisma/migrations/20260116023636_new_model_noti/migration.sql @@ -0,0 +1,111 @@ +/* + Warnings: + + - Added the required column `userId` to the `Article` table without a default value. This is not possible if the table is not empty. + - Added the required column `userId` to the `ArticleComment` table without a default value. This is not possible if the table is not empty. + - Added the required column `userId` to the `Product` table without a default value. This is not possible if the table is not empty. + - Added the required column `userId` to the `ProductComment` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "NotificationType" AS ENUM ('PRODUCT_PRICE_CHANGED', 'ARTICLE_COMMENTED'); + +-- AlterTable +ALTER TABLE "Article" ADD COLUMN "userId" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "ArticleComment" ADD COLUMN "userId" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "Product" ADD COLUMN "userId" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "ProductComment" ADD COLUMN "userId" INTEGER NOT NULL; + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "nickname" TEXT NOT NULL, + "image" TEXT, + "password" TEXT NOT NULL, + "refreshToken" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProductLike" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "productId" INTEGER NOT NULL, + + CONSTRAINT "ProductLike_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ArticleLike" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "articleId" INTEGER NOT NULL, + + CONSTRAINT "ArticleLike_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Notification" ( + "id" SERIAL NOT NULL, + "type" "NotificationType" NOT NULL, + "message" TEXT NOT NULL, + "isRead" BOOLEAN NOT NULL DEFAULT false, + "userId" INTEGER NOT NULL, + "productId" INTEGER, + "articleId" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Notification_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "ProductLike_userId_productId_key" ON "ProductLike"("userId", "productId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ArticleLike_userId_articleId_key" ON "ArticleLike"("userId", "articleId"); + +-- AddForeignKey +ALTER TABLE "Product" ADD CONSTRAINT "Product_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Article" ADD CONSTRAINT "Article_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArticleComment" ADD CONSTRAINT "ArticleComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProductComment" ADD CONSTRAINT "ProductComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProductLike" ADD CONSTRAINT "ProductLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProductLike" ADD CONSTRAINT "ProductLike_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArticleLike" ADD CONSTRAINT "ArticleLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArticleLike" ADD CONSTRAINT "ArticleLike_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Notification" ADD CONSTRAINT "Notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Notification" ADD CONSTRAINT "Notification_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Notification" ADD CONSTRAINT "Notification_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..044d57cd --- /dev/null +++ b/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/prisma/mock.js b/prisma/mock.js new file mode 100644 index 00000000..f95102e4 --- /dev/null +++ b/prisma/mock.js @@ -0,0 +1,79 @@ +// TODO) Mock: DB 데이터 샘플 설정 +import bcrypt from 'bcrypt'; + +const hash = (pw) => bcrypt.hashSync(pw, 10); + +// &) Users +export const mockUsers = Array.from({ length: 50 }, (_, i) => { + const idx = i + 1; + return { + email: `leon${idx}@test.com`, // leon1 ~ 50@test.com + nickname: `Leon${idx}`, // Leon1 ~ 50 + image: null, + password: hash('leon1234'), + }; +}); + +const tagList = [ + 'NONE', + 'FASHION', + 'BEAUTY', + 'SPORTS', + 'ELECTRONICS', + 'HOME_INTERIOR', + 'HOUSEHOLD_SUPPLIES', + 'KITCHENWARE', +]; + +// &) Products +export const mockProducts = Array.from({ length: 50 }, (_, i) => { + const idx = i + 1; + return { + name: `[테스트]상품: ${idx}`, + description: `[테스트]상품 설명: ${idx}`, + price: 1000 * idx, + stock: 10 + (idx % 5), + tags: tagList[idx % tagList.length], + imagePath: `/uploads/sample-${idx}.jpg`, + userIndex: i % mockUsers.length, // 유저 순환 참조 + }; +}); + +// &) Articles +export const mockArticles = Array.from({ length: 50 }, (_, i) => { + const idx = i + 1; + return { + title: `[테스트]게시글: ${idx}`, + content: `[테스트]게시글 설명: ${idx}`, + userIndex: i % mockUsers.length, // 유저 순환 참조 + }; +}); + +// &) Product comments +export const mockProductComments = Array.from({ length: 50 }, (_, i) => { + return { + content: `[테스트]상품 댓글: ${i + 1}`, + productIndex: i % mockProducts.length, // 상품 순환 참조 + userIndex: (i + 1) % mockUsers.length, // 유저 순환 참조 + }; +}); + +// &) Article comments +export const mockArticleComments = Array.from({ length: 50 }, (_, i) => { + return { + content: `[테스트]게시글 댓글: ${i + 1}`, + articleIndex: i % mockArticles.length, // 상품 순환 참조 + userIndex: (i + 2) % mockUsers.length, // 유저 순환 참조 + }; +}); + +// &) Likes: 앞 20개씩 교차 생성 +export const mockProductLikes = Array.from({ length: 50 }, (_, i) => ({ + productIndex: i % mockProducts.length, + userIndex: (i + 3) % mockUsers.length, +})); + +export const mockArticleLikes = Array.from({ length: 50 }, (_, i) => ({ + articleIndex: i % mockArticles.length, + userIndex: (i + 4) % mockUsers.length, +})); diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 00000000..9488c411 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,178 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// 태그 +enum Tag { + NONE + FASHION + BEAUTY + SPORTS + ELECTRONICS + HOME_INTERIOR + HOUSEHOLD_SUPPLIES + KITCHENWARE +} + +// 알림 타입 +enum NotificationType { + PRODUCT_PRICE_CHANGED + ARTICLE_COMMENTED +} + +// 상품 +model Product { + id Int @id @default(autoincrement()) + name String + description String? + price Float + stock Int + tags Tag @default(NONE) + imagePath String? + + // 관계 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int + purchase Purchase[] + productComment ProductComment[] + productLike ProductLike[] + notifications Notification[] + + // 타임스탬프 + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// 게시글 +model Article { + id Int @id @default(autoincrement()) + title String + content String + + // 관계 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int + comment ArticleComment[] + articleLike ArticleLike[] + notifications Notification[] + + // 타임스탬프 = + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// 게시글 댓글 +model ArticleComment { + id Int @id @default(autoincrement()) + content String + + // 관계 + article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) + articleId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int + + // 타임스탬프 + createdAt DateTime @default(now()) +} + +// 상품 댓글 +model ProductComment { + id Int @id @default(autoincrement()) + content String + + // 관계 + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + productId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int + + // 타임스탬프 + createdAt DateTime @default(now()) +} + +// 구매 +model Purchase { + id Int @id @default(autoincrement()) + quantity Int + unitPrice Float + + // 관계 + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + productId Int + + // 타임스탬프 + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// 유저 +model User { + id Int @id @default(autoincrement()) + email String @unique + nickname String + image String? + password String + refreshToken String? // nullable 필수! + + // 관계 + products Product[] + articles Article[] + productComments ProductComment[] + articleComments ArticleComment[] + productLikes ProductLike[] + articleLikes ArticleLike[] + notifications Notification[] + + // 타임스탬프 + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// 상품 좋아요 +model ProductLike { + id Int @id @default(autoincrement()) + + // 관계 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + productId Int + + // 복합유니크 + @@unique([userId, productId]) +} + +// 게시글 좋아요 +model ArticleLike { + id Int @id @default(autoincrement()) + + // 관계 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int + article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) + articleId Int + + // 복합유니크 + @@unique([userId, articleId]) +} + +// 알림 +model Notification { + id Int @id @default(autoincrement()) + type NotificationType + message String + isRead Boolean @default(false) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int + product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) + productId Int? + article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade) + articleId Int? + createdAt DateTime @default(now()) +} diff --git a/prisma/seed.js b/prisma/seed.js new file mode 100644 index 00000000..924f3b94 --- /dev/null +++ b/prisma/seed.js @@ -0,0 +1,129 @@ +// TODO) Seed: DB 데이터 삽입 설정 +import prisma from '../src/config/prisma.js'; +import { + mockUsers, + mockProducts, + mockArticles, + mockProductComments, + mockArticleComments, + mockProductLikes, + mockArticleLikes, +} from './mock.js'; + +// &) 삭제 순서: 좋아요 → 댓글 → 구매 → 게시글/상품 → 유저 +async function main() { + await prisma.productLike.deleteMany(); + await prisma.articleLike.deleteMany(); + await prisma.productComment.deleteMany(); + await prisma.articleComment.deleteMany(); + await prisma.purchase.deleteMany(); + await prisma.product.deleteMany(); + await prisma.article.deleteMany(); + await prisma.user.deleteMany(); + + // &) 유저 생성 + await prisma.user.createMany({ data: mockUsers }); + + // &) ID 매핑 위해 다시 조회 (생성 순서대로 정렬) + const userEntities = await prisma.user.findMany({ + select: { id: true }, + orderBy: { id: 'asc' }, + }); + + const withUserId = (arr, key) => + arr.map((item) => ({ + ...item, + [key]: userEntities[item.userIndex].id, + })); + + // &) 상품/게시글 생성 + await prisma.product.createMany({ + data: withUserId(mockProducts, 'userId').map( + ({ userIndex, ...rest }) => rest + ), + }); + await prisma.article.createMany({ + data: withUserId(mockArticles, 'userId').map( + ({ userIndex, ...rest }) => rest + ), + }); + + // &) 생성된 상품/게시글 id 조회 + const products = await prisma.product.findMany({ + select: { id: true }, + orderBy: { id: 'asc' }, + }); + const articles = await prisma.article.findMany({ + select: { id: true }, + orderBy: { id: 'asc' }, + }); + + const mapComment = (arr, target) => + arr.map((item) => ({ + content: item.content, + userId: userEntities[item.userIndex].id, + [`${target}Id`]: + target === 'product' + ? products[item.productIndex].id + : articles[item.articleIndex].id, + })); + + // &) 댓글 생성 + await prisma.productComment.createMany({ + data: mapComment(mockProductComments, 'product'), + }); + await prisma.articleComment.createMany({ + data: mapComment(mockArticleComments, 'article'), + }); + + // &) 좋아요 생성 + await prisma.productLike.createMany({ + data: mockProductLikes.map((item) => ({ + userId: userEntities[item.userIndex].id, + productId: products[item.productIndex].id, + })), + }); + + await prisma.articleLike.createMany({ + data: mockArticleLikes.map((item) => ({ + userId: userEntities[item.userIndex].id, + articleId: articles[item.articleIndex].id, + })), + }); + + // &) 시드 카운트 + const counts = await Promise.all([ + prisma.user.count(), + prisma.product.count(), + prisma.article.count(), + prisma.productComment.count(), + prisma.articleComment.count(), + prisma.productLike.count(), + prisma.articleLike.count(), + ]); + + console.log( + `🌱 Seed-Success: + - [users] ${counts[0]} + - [products] ${counts[1]} + - [articles] ${counts[2]}` + ); + console.log( + `🌱 Seed-Success: + - [productComments] ${counts[3]} + - [articleComments] ${counts[4]} + - [productLikes] ${counts[5]} + - [articleLikes] ${counts[6]}` + ); +} + +// &) 연결 강제 종료 +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(`시드 실패: ${e}`); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/public/uploads/.gitkeep b/public/uploads/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/__tests__/article.auth.test.ts b/src/__tests__/article.auth.test.ts new file mode 100644 index 00000000..25cedbb0 --- /dev/null +++ b/src/__tests__/article.auth.test.ts @@ -0,0 +1,57 @@ +import request from 'supertest'; +import prisma from '../config/prisma.js'; +import { app } from '../app.js'; +import { createUserWithToken, resetDb } from '../test/test-helpers.js'; + +describe('Article API (인증 필요)', () => { + beforeAll(async () => { + await resetDb(); + }); + + test('POST /articles 인증 없으면 거부', async () => { + const res = await request(app).post('/articles').send({ + title: 'auth article', + content: 'content', + }); + + expect(res.status).toBe(401); + }); + + test('POST /articles 게시글 생성', async () => { + const { accessToken } = await createUserWithToken( + 'auth-article-create@example.com' + ); + + const res = await request(app) + .post('/articles') + .set('Authorization', `Bearer ${accessToken}`) + .send({ + title: 'auth article', + content: 'content', + }); + + expect(res.status).toBe(201); + expect(res.body.success).toBe(true); + }); + + test('PATCH /articles/:id 게시글 수정', async () => { + const { user, accessToken } = await createUserWithToken( + 'auth-article-update@example.com' + ); + const article = await prisma.article.create({ + data: { + title: 'auth article', + content: 'content', + userId: user.id, + }, + }); + + const res = await request(app) + .patch(`/articles/${article.id}`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ title: 'updated title' }); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + }); +}); diff --git a/src/__tests__/article.public.test.ts b/src/__tests__/article.public.test.ts new file mode 100644 index 00000000..dcee4944 --- /dev/null +++ b/src/__tests__/article.public.test.ts @@ -0,0 +1,35 @@ +import request from 'supertest'; +import { app } from '../app.js'; +import prisma from '../config/prisma.js'; +import { createUserWithToken, resetDb } from '../test/test-helpers.js'; + +describe('Article API (public)', () => { + beforeAll(async () => { + await resetDb(); + }); + + test('GET /articles 게시글 목록', async () => { + const res = await request(app).get('/articles'); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(Array.isArray(res.body.data)).toBe(true); + }); + + test('GET /articles/:id 게시글 상세 조회', async () => { + const { user } = await createUserWithToken('public-article@example.com'); + const article = await prisma.article.create({ + data: { + title: 'public article', + content: 'content', + userId: user.id, + }, + }); + + const res = await request(app).get(`/articles/${article.id}`); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.data.id).toBe(article.id); + }); +}); diff --git a/src/__tests__/article.service.unit.test.ts b/src/__tests__/article.service.unit.test.ts new file mode 100644 index 00000000..a0c79829 --- /dev/null +++ b/src/__tests__/article.service.unit.test.ts @@ -0,0 +1,43 @@ +import { jest } from '@jest/globals'; +import { ValidationError } from '../core/error/error-handler.js'; +import { articleRepo } from '../repositories/article-repository.js'; +import { articleService } from '../services/article-service.js'; + +describe('Article service (유닛 단위 테스트)', () => { + beforeEach(() => { + jest.restoreAllMocks(); + }); + + test('목록 조회는 정렬/검색 조건을 저장소에 전달한다', async () => { + const spy = jest.spyOn(articleRepo, 'findArticles').mockResolvedValue([]); + + await articleService.list({ + q: '테스트', + offset: '0', + limit: '10', + order: 'recent', + }); + + expect(spy).toHaveBeenCalledTimes(1); + const [where, orderBy, skip, take] = spy.mock.calls[0] as Parameters< + typeof articleRepo.findArticles + >; + + expect(where).toEqual( + expect.objectContaining({ + OR: expect.any(Array), + }) + ); + expect(orderBy).toEqual({ createdAt: 'desc' }); + expect(skip).toBe(0); + expect(take).toBe(10); + }); + + test('지원하지 않는 정렬 키면 ValidationError를 던진다', async () => { + await expect( + articleService.list({ + order: 'unknown-order', + }) + ).rejects.toBeInstanceOf(ValidationError); + }); +}); diff --git a/src/__tests__/auth.test.ts b/src/__tests__/auth.test.ts new file mode 100644 index 00000000..2f7f0408 --- /dev/null +++ b/src/__tests__/auth.test.ts @@ -0,0 +1,47 @@ +import request from 'supertest'; +import { app } from '../app.js'; +import { resetDb } from '../test/test-helpers.js'; + +describe('Auth API', () => { + beforeAll(async () => { + await resetDb(); + }); + + test('POST /users/register 회원가입', async () => { + const res = await request(app).post('/users/register').send({ + email: 'auth-register@example.com', + password: 'password1234', + nickname: 'tester', + image: null, + }); + + expect(res.status).toBe(201); + expect(res.body.success).toBe(true); + expect(res.body.data.user.email).toBe('auth-register@example.com'); + }); + + test('POST /users/login 로그인', async () => { + await request(app).post('/users/register').send({ + email: 'auth-login@example.com', + password: 'password1234', + nickname: 'tester', + image: null, + }); + + const res = await request(app).post('/users/login').send({ + email: 'auth-login@example.com', + password: 'password1234', + }); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + const setCookieHeader = res.headers['set-cookie']; + const cookies = Array.isArray(setCookieHeader) + ? setCookieHeader + : setCookieHeader + ? [setCookieHeader] + : []; + expect(cookies.join(';')).toContain('accessToken='); + expect(cookies.join(';')).toContain('refreshToken='); + }); +}); diff --git a/src/__tests__/product.auth.test.ts b/src/__tests__/product.auth.test.ts new file mode 100644 index 00000000..d6028f25 --- /dev/null +++ b/src/__tests__/product.auth.test.ts @@ -0,0 +1,69 @@ +import request from 'supertest'; +import { app } from '../app.js'; +import prisma from '../config/prisma.js'; +import { createUserWithToken, resetDb } from '../test/test-helpers.js'; + +describe('Product API (인증 필요)', () => { + beforeAll(async () => { + await resetDb(); + }); + + test('POST /products 인증 없으면 거부', async () => { + const res = await request(app).post('/products').send({ + name: 'auth product', + description: 'desc', + price: 1000, + stock: 2, + tags: 'NONE', + imagePath: 'test.png', + }); + + expect(res.status).toBe(401); + }); + + test('POST /products 상품 생성', async () => { + const { accessToken } = await createUserWithToken( + 'auth-product-create@example.com' + ); + + const res = await request(app) + .post('/products') + .set('Authorization', `Bearer ${accessToken}`) + .send({ + name: 'auth product', + description: 'desc', + price: 1000, + stock: 2, + tags: 'NONE', + imagePath: 'test.png', + }); + + expect(res.status).toBe(201); + expect(res.body.success).toBe(true); + }); + + test('PATCH /products/:id 상품 수정', async () => { + const { user, accessToken } = await createUserWithToken( + 'auth-product-update@example.com' + ); + const product = await prisma.product.create({ + data: { + name: 'auth product', + description: 'desc', + price: 1000, + stock: 2, + tags: 'NONE', + imagePath: 'test.png', + userId: user.id, + }, + }); + + const res = await request(app) + .patch(`/products/${product.id}`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ price: 2000 }); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + }); +}); diff --git a/src/__tests__/product.public.test.ts b/src/__tests__/product.public.test.ts new file mode 100644 index 00000000..1d86bf68 --- /dev/null +++ b/src/__tests__/product.public.test.ts @@ -0,0 +1,40 @@ +import request from 'supertest'; +import { Tag } from '@prisma/client'; +import { app } from '../app.js'; +import prisma from '../config/prisma.js'; +import { createUserWithToken, resetDb } from '../test/test-helpers.js'; + +describe('Product API (public)', () => { + beforeAll(async () => { + await resetDb(); + }); + + test('GET /products 상품 목록', async () => { + const res = await request(app).get('/products'); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(Array.isArray(res.body.data)).toBe(true); + }); + + test('GET /products/:id 상품 상세 조회', async () => { + const { user } = await createUserWithToken('public-product@example.com'); + const product = await prisma.product.create({ + data: { + name: 'public product', + description: 'desc', + price: 1000, + stock: 3, + tags: Tag.NONE, + imagePath: 'test.png', + userId: user.id, + }, + }); + + const res = await request(app).get(`/products/${product.id}`); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.data.id).toBe(product.id); + }); +}); diff --git a/src/__tests__/product.service.unit.test.ts b/src/__tests__/product.service.unit.test.ts new file mode 100644 index 00000000..2615efbb --- /dev/null +++ b/src/__tests__/product.service.unit.test.ts @@ -0,0 +1,33 @@ +import { jest } from '@jest/globals'; +import { productService } from '../services/product-service.js'; +import { productRepo } from '../repositories/product-repository.js'; + +describe('Product service (유닛 단위 테스트)', () => { + beforeEach(() => { + jest.restoreAllMocks(); + }); + + test('목록 조회는 필터링된 쿼리로 저장소를 호출한다', async () => { + const spy = jest.spyOn(productRepo, 'findProducts').mockResolvedValue([]); + + await productService.list({ + q: '테스트', + tag: 'FASHION', + offset: '0', + limit: '10', + order: 'recent', + }); + + expect(spy).toHaveBeenCalledTimes(1); + const call = spy.mock.calls[0] as Parameters< + typeof productRepo.findProducts + >; + const [where] = call; + expect(where).toEqual( + expect.objectContaining({ + tags: 'FASHION', + OR: expect.any(Array), + }) + ); + }); +}); diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 00000000..57650948 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,74 @@ +// TODO) App: 서버 진입점 +// &) Config Import +import './config/env.js'; // 맨 위 필수! +import express from 'express'; +import cors from 'cors'; +import path from 'path'; +import cookieParser from 'cookie-parser'; +import http from 'http'; + +// &) Core Import +import { debugLog } from './core/error/debug.js'; +import { errorHandler, notFoundHandler } from './core/error/error-handler.js'; + +// &) Route Import +import healthRoutes from './routes/health-routes.js'; +import uploadRoutes from './routes/upload-routes.js'; +import productsRoutes from './routes/product-routes.js'; +import articlesRoutes from './routes/article-routes.js'; +import productCommentsRoutes from './routes/product-comment-routes.js'; +import articleCommentsRoutes from './routes/article-comment-routes.js'; +import userRoutes from './routes/user-routes.js'; +import productLikeRoutes from './routes/product-like-routes.js'; +import articleLikeRoutes from './routes/article-like-routes.js'; +import notificationRoutes from './routes/notification-routes.js'; +import { initSocket } from './socket/io.js'; + +// ?) 환경 변수 +const PORT = Number(process.env.PORT ?? 4000); + +// ?) Express 진입 +const app = express(); +const server = http.createServer(app); + +// ?) 미들 웨어 진입 +app.use(cors()); +app.use(express.json()); +app.use(cookieParser()); + +// ?) 이미지 정적 경로 진입 +app.use( + '/uploads', + express.static(path.join(process.cwd(), 'public', 'uploads')) +); + +// ?) 라우터 진입 (핵심) +app.use('/health', healthRoutes); // 헬스 체크 +app.use('/upload', uploadRoutes); // 이미지 +app.use('/products', productsRoutes); // 상품 +app.use('/articles', articlesRoutes); // 게시글 +app.use('/product-comments', productCommentsRoutes); // 상품 댓글 +app.use('/article-comments', articleCommentsRoutes); // 게시글 댓글 +app.use('/users', userRoutes); // 유저 +app.use('/product-likes', productLikeRoutes); // 상품 좋아요 +app.use('/article-likes', articleLikeRoutes); // 게시글 좋아요 +app.use('/notifications', notificationRoutes); // 알림 + +// ?) 404 핸들러 진입 +app.use(notFoundHandler); + +// ?) 전역 에러 핸들러 진입 (맨 마지막!) +app.use(errorHandler); + +// ?) 서버 실행 진입 +initSocket(server); + +if (process.env.NODE_ENV !== 'test') { + server.listen(PORT, () => { + console.log(`🚀 Server is running on port http://localhost:${PORT}`); + debugLog('Debug mode is enabled'); + debugLog(`Environment: ${process.env.NODE_ENV || 'development'}`); + }); +} + +export { app, server }; diff --git a/src/config/env.ts b/src/config/env.ts new file mode 100644 index 00000000..8060fb8f --- /dev/null +++ b/src/config/env.ts @@ -0,0 +1,14 @@ +// TODO) Env-Loader: 환경, 설정, 공통 미들웨어 정의 +// ?) 모든 모듈에서 .env를 확실하게 읽도록 공용 로더 제공 +import dotenv from 'dotenv'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// 1) ESM 환경 경로 설정 +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// 2) 루트 위치 .env 파일 명시적으로 로드 +dotenv.config({ + path: path.resolve(__dirname, '../../.env'), +}); diff --git a/src/config/multer.ts b/src/config/multer.ts new file mode 100644 index 00000000..57d40a34 --- /dev/null +++ b/src/config/multer.ts @@ -0,0 +1,45 @@ +// TODO) Multer-Loader: 환경, 설정, 공통 미들웨어 정의 +// ?) 이미지 업로드 설정 +import multer, { type FileFilterCallback } from 'multer'; +import path from 'path'; +import crypto from 'crypto'; +import fs from 'fs'; +import type { Request } from 'express'; + +// 1) 파일 크기 제한 +const limits = { fileSize: 5 * 1024 * 1024 }; // 5MB + +// 2) 업로드 폴더 생성 (없으면 자동 생성) -> 업로드 저장소 만들기 위한 목적 +const uploadDir = path.join(process.cwd(), 'public', 'uploads'); +fs.mkdirSync(uploadDir, { recursive: true }); + +// 3) 파일 저장 방식 설정 +const storage = multer.diskStorage({ + destination: (_req, _file, cb) => cb(null, uploadDir), + filename: (_req, file, cb) => { + const ext = path.extname(file.originalname).toLowerCase(); + const validExt = ['.jpg', '.jpeg', '.png'].includes(ext) ? ext : ''; + cb(null, crypto.randomUUID() + validExt); + }, +}); + +// 4) 허용 MIME 타입 필터링 +const fileFilter = ( + _req: Request, + file: Express.Multer.File, + cb: FileFilterCallback +) => { + const isValidType = ['image/jpeg', 'image/png'].includes(file.mimetype); + if (isValidType) { + cb(null, true); + } else { + cb(new Error('jpeg/png만 업로드 가능합니다.')); + } +}; + +// 5) 최종 업로드 인스턴스 +export const upload = multer({ + storage, + limits, + fileFilter, +}); diff --git a/src/config/prisma.ts b/src/config/prisma.ts new file mode 100644 index 00000000..4d41fb9f --- /dev/null +++ b/src/config/prisma.ts @@ -0,0 +1,16 @@ +// TODO) Prisma-Singleton: 환경, 설정, 공통 미들웨어 정의 +// ?) PrismaClient DB 연결 중복 예방 +import './env.js'; +// 1) PrismaClient가 생성되기 전에 .env를 반드시 로드해야 오류 안터짐! +import { PrismaClient } from '@prisma/client'; + +// 2) 새로운 PrismaClient 생성 +const prisma = new PrismaClient({ + // 3) 로그 옵션 + log: + process.env.DEBUG_MODE === 'true' + ? ['query', 'info', 'warn', 'error'] // ← 개발: 모든 로그 출력 + : ['error'], // ← 배포: 에러 로그만 출력 +}); + +export default prisma; diff --git a/src/constants/article.ts b/src/constants/article.ts new file mode 100644 index 00000000..ffdc1fb4 --- /dev/null +++ b/src/constants/article.ts @@ -0,0 +1,15 @@ +// TODO) Article-Constants: 게시글 관련 공통 상수 +export const ARTICLE_ORDER = { + RECENT: 'recent', + OLDEST: 'oldest', +} as const; + +export const ARTICLE_ORDER_MAP: Record< + (typeof ARTICLE_ORDER)[keyof typeof ARTICLE_ORDER], + { createdAt: 'asc' | 'desc' } +> = { + [ARTICLE_ORDER.RECENT]: { createdAt: 'desc' }, + [ARTICLE_ORDER.OLDEST]: { createdAt: 'asc' }, +}; + +export const DEFAULT_ARTICLE_ORDER = ARTICLE_ORDER.RECENT; diff --git a/src/constants/product.ts b/src/constants/product.ts new file mode 100644 index 00000000..66c9bd29 --- /dev/null +++ b/src/constants/product.ts @@ -0,0 +1,15 @@ +// TODO) Product-Constants: 상품 관련 공통 상수 +export const PRODUCT_ORDER = { + RECENT: 'recent', + OLDEST: 'oldest', +} as const; + +export const PRODUCT_ORDER_MAP: Record< + (typeof PRODUCT_ORDER)[keyof typeof PRODUCT_ORDER], + { createdAt: 'asc' | 'desc' } +> = { + [PRODUCT_ORDER.RECENT]: { createdAt: 'desc' }, + [PRODUCT_ORDER.OLDEST]: { createdAt: 'asc' }, +}; + +export const DEFAULT_PRODUCT_ORDER = PRODUCT_ORDER.RECENT; diff --git a/src/controllers/article-comment-controller.ts b/src/controllers/article-comment-controller.ts new file mode 100644 index 00000000..4ad2b8a3 --- /dev/null +++ b/src/controllers/article-comment-controller.ts @@ -0,0 +1,55 @@ +// TODO) Article-Comment-Controller: 요청 처리 +import type { Request, Response } from 'express'; +import { articleCommentService } from '../services/article-comment-service.js'; + +export const articleCommentController = { + // 1) 댓글 목록 조회 + async list(req: Request, res: Response) { + const articleId = Number(req.params.articleId); + const comments = await articleCommentService.list(articleId); + + res.status(200).json({ + success: true, + message: '게시글 댓글 목록 조회 성공', + data: comments, + }); + }, + + // 2) 댓글 생성 + async create(req: Request, res: Response) { + const comment = await articleCommentService.create(req.body, req.user!.id); + + res.status(201).json({ + success: true, + message: '게시글 댓글이 등록되었습니다', + data: comment, + }); + }, + + // 3) 댓글 수정 + async update(req: Request, res: Response) { + const commentId = Number(req.params.id); + const comment = await articleCommentService.update( + commentId, + req.body.content, + req.user!.id + ); + + res.status(200).json({ + success: true, + message: '게시글 댓글이 수정되었습니다', + data: comment, + }); + }, + + // 4) 댓글 삭제 + async remove(req: Request, res: Response) { + const commentId = Number(req.params.id); + await articleCommentService.remove(commentId, req.user!.id); + + res.status(200).json({ + success: true, + message: '게시글 댓글이 삭제되었습니다', + }); + }, +}; diff --git a/src/controllers/article-controller.ts b/src/controllers/article-controller.ts new file mode 100644 index 00000000..8ffc131d --- /dev/null +++ b/src/controllers/article-controller.ts @@ -0,0 +1,70 @@ +// TODO) Article-Controller: 요청 처리 +import type { Request, Response } from 'express'; +import { articleService } from '../services/article-service.js'; + +export const articleController = { + // 1) 게시글 목록 조회 + async list(req: Request, res: Response) { + const articles = await articleService.list(req.query); + + res.status(200).json({ + success: true, + message: '게시글 목록 조회 성공', + data: articles, + }); + }, + + // 2) 게시글 조회 + async detail(req: Request, res: Response) { + const articleId = Number(req.params.id); + const article = await articleService.getOrThrow(articleId); + const userId = req.user?.id; + const liked = userId + ? await articleService.isLiked(userId, article.id) + : false; + + res.status(200).json({ + success: true, + message: '게시글 조회 성공', + data: { ...article, isLiked: liked }, + }); + }, + + // 3) 게시글 생성 + async create(req: Request, res: Response) { + const article = await articleService.create(req.body, req.user!.id); + + res.status(201).json({ + success: true, + message: '게시글이 등록되었습니다', + data: article, + }); + }, + + // 4) 게시글 수정 + async update(req: Request, res: Response) { + const articleId = Number(req.params.id); + const article = await articleService.update( + articleId, + req.body, + req.user!.id + ); + + res.status(200).json({ + success: true, + message: '게시글이 수정되었습니다', + data: article, + }); + }, + + // 5) 게시글 삭제 + async remove(req: Request, res: Response) { + const articleId = Number(req.params.id); + await articleService.remove(articleId, req.user!.id); + + res.status(200).json({ + success: true, + message: '게시글이 삭제되었습니다', + }); + }, +}; diff --git a/src/controllers/article-like-controller.ts b/src/controllers/article-like-controller.ts new file mode 100644 index 00000000..8223788e --- /dev/null +++ b/src/controllers/article-like-controller.ts @@ -0,0 +1,40 @@ +// TODO) Article-Like-Controller: 요청 처리 +import type { Request, Response } from 'express'; +import { articleLikeService } from '../services/article-like-service.js'; + +export const articleLikeController = { + // 1) 게시글 좋아요 등록 + async like(req: Request, res: Response) { + const articleId = Number(req.params.id); + const result = await articleLikeService.like(req.user!.id, articleId); + + res.status(201).json({ + success: true, + message: '게시글 좋아요 완료', + data: result, + }); + }, + + // 2) 게시글 좋아요 취소 + async unlike(req: Request, res: Response) { + const articleId = Number(req.params.id); + const result = await articleLikeService.unlike(req.user!.id, articleId); + + res.status(200).json({ + success: true, + message: '게시글 좋아요 취소 완료', + data: result, + }); + }, + + // 3) 좋아요한 게시글 조회 + async list(req: Request, res: Response) { + const liked = await articleLikeService.list(req.user!.id); + + res.status(200).json({ + success: true, + message: '좋아요한 게시글 목록 조회 성공', + data: liked.map((item) => item.article), + }); + }, +}; diff --git a/src/controllers/health-controller.ts b/src/controllers/health-controller.ts new file mode 100644 index 00000000..088e874f --- /dev/null +++ b/src/controllers/health-controller.ts @@ -0,0 +1,33 @@ +// TODO) Helath-Controller: 요청 처리 +import type { Request, Response } from 'express'; +import prisma from '../config/prisma.js'; + +// 1) 서버 연결 확인 +export const checkHealth = async (_req: Request, res: Response) => { + res.status(200).json({ + success: true, + message: '서버 연결 성공', + timestamp: new Date().toISOString(), + environment: process.env.NODE_ENV || 'development', + }); +}; + +// 2) DB 연결 확인 +export const checkDatabase = async (_req: Request, res: Response) => { + try { + await prisma.$queryRaw`SELECT 1`; + res.status(200).json({ + success: true, + message: 'DB 연결 성공', + timestamp: new Date().toISOString(), + }); + } catch (error) { + const message = + error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다'; + res.status(500).json({ + success: false, + message: 'DB 연결 실패', + error: message, + }); + } +}; diff --git a/src/controllers/notification-controller.ts b/src/controllers/notification-controller.ts new file mode 100644 index 00000000..e9675505 --- /dev/null +++ b/src/controllers/notification-controller.ts @@ -0,0 +1,48 @@ +// TODO) Notification-Controller: 요청 처리 +import type { Request, Response } from 'express'; +import { notificationService } from '../services/notification-service.js'; + +export const notificationController = { + // 1) 알림 목록 조회 + async list(req: Request, res: Response) { + const notifications = await notificationService.list(req.user!.id); + + res.status(200).json({ + success: true, + message: '알림 목록 조회 성공', + data: notifications, + }); + }, + + // 2) 미읽음 개수 조회 + async unreadCount(req: Request, res: Response) { + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ + success: false, + message: '인증 정보가 없습니다', + }); + } + + const count = await notificationService.countUnread(userId); + + res.status(200).json({ + success: true, + message: '미읽음 알림 개수 조회 성공', + data: { count }, + }); + }, + + // 3) 읽음 처리 + async markRead(req: Request, res: Response) { + const id = Number(req.params.id); + const notification = await notificationService.markRead(id, req.user!.id); + + res.status(200).json({ + success: true, + message: '알림을 읽음 처리했습니다', + data: notification, + }); + }, +}; diff --git a/src/controllers/product-comment-controller.ts b/src/controllers/product-comment-controller.ts new file mode 100644 index 00000000..17e37f71 --- /dev/null +++ b/src/controllers/product-comment-controller.ts @@ -0,0 +1,55 @@ +// TODO) Product-Comment-Controller: 요청 처리 +import type { Request, Response } from 'express'; +import { productCommentService } from '../services/product-comment-service.js'; + +export const productCommentController = { + // 1) 댓글 목록 조회 + async list(req: Request, res: Response) { + const productId = Number(req.params.productId); + const comments = await productCommentService.list(productId); + + res.status(200).json({ + success: true, + message: '상품 댓글 목록 조회 성공', + data: comments, + }); + }, + + // 2) 댓글 생성 + async create(req: Request, res: Response) { + const comment = await productCommentService.create(req.body, req.user!.id); + + res.status(201).json({ + success: true, + message: '상품 댓글이 등록되었습니다', + data: comment, + }); + }, + + // 3) 댓글 수정 + async update(req: Request, res: Response) { + const commentId = Number(req.params.id); + const comment = await productCommentService.update( + commentId, + req.body.content, + req.user!.id + ); + + res.status(200).json({ + success: true, + message: '상품 댓글이 수정되었습니다', + data: comment, + }); + }, + + // 4) 댓글 삭제 + async remove(req: Request, res: Response) { + const commentId = Number(req.params.id); + await productCommentService.remove(commentId, req.user!.id); + + res.status(200).json({ + success: true, + message: '상품 댓글이 삭제되었습니다', + }); + }, +}; diff --git a/src/controllers/product-controller.ts b/src/controllers/product-controller.ts new file mode 100644 index 00000000..db6ab0bd --- /dev/null +++ b/src/controllers/product-controller.ts @@ -0,0 +1,82 @@ +// TODO) Product-Controller: 요청 처리 +import type { Request, Response } from 'express'; +import { productService } from '../services/product-service.js'; + +export const productController = { + // 1) 상품 목록 조회 + async list(req: Request, res: Response) { + const products = await productService.list(req.query); + + res.status(200).json({ + success: true, + message: '상품 목록 조회 성공', + data: products, + }); + }, + + // 2) 상품 조회 + async detail(req: Request, res: Response) { + const productId = Number(req.params.id); + const product = await productService.getOrThrow(productId); + const userId = req.user?.id; + const liked = userId + ? await productService.isLiked(userId, product.id) + : false; + + res.status(200).json({ + success: true, + message: '상품 조회 성공', + data: { ...product, isLiked: liked }, + }); + }, + + // 3) 상품 생성 + async create(req: Request, res: Response) { + const product = await productService.create(req.body, req.user!.id); + + res.status(201).json({ + success: true, + message: '상품이 등록되었습니다', + data: product, + }); + }, + + // 4) 상품 수정 + async update(req: Request, res: Response) { + const productId = Number(req.params.id); + const product = await productService.update( + productId, + req.body, + req.user!.id + ); + + res.status(200).json({ + success: true, + message: '상품이 수정되었습니다', + data: product, + }); + }, + + // 5) 상품 삭제 + async remove(req: Request, res: Response) { + const productId = Number(req.params.id); + await productService.remove(productId, req.user!.id); + + res.status(200).json({ + success: true, + message: '상품이 삭제되었습니다', + }); + }, + + // 6) 상품 구매 + async purchase(req: Request, res: Response) { + const { productId, quantity } = req.body; + const result = await productService.purchase(productId, quantity); + + res.status(201).json({ + success: true, + message: '상품 구매가 완료되었습니다', + data: result, + }); + }, +}; diff --git a/src/controllers/product-like-controller.ts b/src/controllers/product-like-controller.ts new file mode 100644 index 00000000..18aeb2eb --- /dev/null +++ b/src/controllers/product-like-controller.ts @@ -0,0 +1,40 @@ +// TODO) Product-Like-Controller: 요청 처리 +import type { Request, Response } from 'express'; +import { productLikeService } from '../services/product-like-service.js'; + +export const productLikeController = { + // 1) 상품 좋아요 등록 + async like(req: Request, res: Response) { + const productId = Number(req.params.id); + const result = await productLikeService.like(req.user!.id, productId); + + res.status(201).json({ + success: true, + message: '상품 좋아요 완료', + data: result, + }); + }, + + // 2) 상품 좋아요 취소 + async unlike(req: Request, res: Response) { + const productId = Number(req.params.id); + const result = await productLikeService.unlike(req.user!.id, productId); + + res.status(200).json({ + success: true, + message: '상품 좋아요 취소 완료', + data: result, + }); + }, + + // 3) 좋아요한 상품 조회 + async list(req: Request, res: Response) { + const liked = await productLikeService.list(req.user!.id); + + res.status(200).json({ + success: true, + message: '좋아요한 상품 목록 조회 성공', + data: liked.map((item) => item.product), + }); + }, +}; diff --git a/src/controllers/upload-controller.ts b/src/controllers/upload-controller.ts new file mode 100644 index 00000000..aa25813c --- /dev/null +++ b/src/controllers/upload-controller.ts @@ -0,0 +1,16 @@ +// TODO) Upload-Controller: 요청 처리 +import type { Request, Response, NextFunction } from 'express'; +import { ValidationError } from '../core/error/error-handler.js'; + +export const uploadController = { + image(req: Request, res: Response, next: NextFunction) { + if (!req.file) { + return next(new ValidationError('file', '업로드된 파일이 없습니다')); + } + + return res.status(201).json({ + success: true, + path: `/uploads/${req.file.filename}`, + }); + }, +}; diff --git a/src/controllers/user-controller.ts b/src/controllers/user-controller.ts new file mode 100644 index 00000000..2c2c7abf --- /dev/null +++ b/src/controllers/user-controller.ts @@ -0,0 +1,198 @@ +// TODO) User-Controller: 요청 처리 +import type { Request, Response } from 'express'; +import { userService } from '../services/user-service.js'; +import { authService } from '../services/auth-service.js'; +import { productService } from '../services/product-service.js'; + +export const userController = { + // 1) 회원가입 + async register(req: Request, res: Response) { + const { email, password, nickname, image } = req.body; + const user = await userService.registerUser({ + email, + password, + nickname, + image, + }); + + // 1-1) 토큰 생성 + const tokens = await authService.generateTokens(user); + + // 1-2) 토큰 생성 시 쿠키 옵션 설정 + if (tokens.refreshToken) { + res.cookie('refreshToken', tokens.refreshToken, { + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + }); + } + + if (tokens.accessToken) { + res.cookie('accessToken', tokens.accessToken, { + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + }); + } + + res.status(201).json({ + success: true, + message: '회원가입 완료', + data: { + user: { + id: user.id, + email: user.email, + nickname: user.nickname, + image: user.image, + }, + }, + }); + }, + + // 2) 로그인 + async login(req: Request, res: Response) { + const { email, password } = req.body; + const user = await userService.loginUser(email, password); + + // 2-1) 토큰 생성 + const tokens = await authService.generateTokens(user); + + // 2-2) 토큰 생성 시 쿠키 옵션 설정 + if (tokens.refreshToken) { + res.cookie('refreshToken', tokens.refreshToken, { + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + }); + } + + if (tokens.accessToken) { + res.cookie('accessToken', tokens.accessToken, { + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + }); + } + + res.status(200).json({ + success: true, + message: '로그인 성공', + data: { + user: { + id: user.id, + email: user.email, + nickname: user.nickname, + image: user.image, + }, + }, + }); + }, + + // 3) 토큰 재발급 + async refresh(req: Request, res: Response) { + // 3-1) 토큰 찾기 (쿠키 먼저, 없을 시 바디) + const refreshToken = (req.cookies?.refreshToken || + req.body?.refreshToken) as string | undefined; + + // 3-2) 토큰 검증 + if (!refreshToken) { + return res.status(400).json({ + success: false, + message: 'refreshToken이 없습니다', + }); + } + + // 3-3) 토큰 생성 + const accessToken = await authService.rotateAccessToken(refreshToken); + + res.status(200).json({ + success: true, + message: '토큰 재발급 성공', + accessToken, + }); + }, + + // 4) 로그아웃 + async logout(req: Request, res: Response) { + // 4-1) 사용자 ID 추출 및 검증 + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ + success: false, + message: '인증 정보가 없습니다', + }); + } + + // 4-2) DB 토큰 제거 + await authService.clearRefreshToken(userId); + + // 4-3) 브라우저 쿠키 제거 + res.clearCookie('refreshToken'); + + res.status(200).json({ + success: true, + message: '로그아웃 완료', + }); + }, + + // 5) 내 정보 조회 + async me(req: Request, res: Response) { + const user = await userService.getMe(req.user!.id); + + res.status(200).json({ + success: true, + message: '인증 성공', + data: user, + }); + }, + + // 6) 프로필 수정 + async updateName(req: Request, res: Response) { + const profile = await userService.changeProfile(req.user!.id, { + nickname: req.body.nickname, + image: req.body.image, + }); + + res.status(200).json({ + success: true, + message: '프로필이 변경되었습니다', + data: profile, + }); + }, + + // 7) 비밀번호 변경 + async updatePassword(req: Request, res: Response) { + await userService.changePassword( + req.user!.id, + req.body.oldPw, + req.body.newPw + ); + + res.status(200).json({ + success: true, + message: '비밀번호가 변경되었습니다', + }); + }, + + // 8) 회원 탈퇴 + async removeAccount(req: Request, res: Response) { + await userService.deleteAccount(req.user!.id); + + res.status(200).json({ + success: true, + message: '계정이 삭제되었습니다', + }); + }, + + // 9) 내가 등록한 상품 조회 + async myProducts(req: Request, res: Response) { + const products = await productService.listByUser(req.user!.id); + + res.status(200).json({ + success: true, + message: '내가 등록한 상품 목록 조회 성공', + data: products, + }); + }, +}; diff --git a/src/core/error/async-handler.ts b/src/core/error/async-handler.ts new file mode 100644 index 00000000..bb25285a --- /dev/null +++ b/src/core/error/async-handler.ts @@ -0,0 +1,19 @@ +// TODO) Async-Handler: 비동기 라우터 에러 자동 처리 래퍼 +// ?) Express 비동기 컨트롤러에서 발생하는 에러를 자동으로 next(err)로 전달하는 헬퍼 +import type { NextFunction, Request, Response } from 'express'; + +type AsyncController = ( + req: Request, + res: Response, + next: NextFunction +) => Promise | unknown; + +const asyncHandler = + (fn: AsyncController) => + (req: Request, res: Response, next: NextFunction) => { + // fn(req, res, next)가 Promise이므로 자동으로 resolve/catch 가능 + Promise.resolve(fn(req, res, next)).catch(next); + // catch(next) → 오류 발생 시 Express 에러 핸들러로 넘김 + }; + +export default asyncHandler; diff --git a/src/core/error/debug.ts b/src/core/error/debug.ts new file mode 100644 index 00000000..e37f6740 --- /dev/null +++ b/src/core/error/debug.ts @@ -0,0 +1,95 @@ +// TODO) Debug: 디버그 전용 유틸 (배포 환경에서는 반드시 꺼야 함) +// ?) DEBUG_MODE = true 일 때만 모든 디버그 출력이 동작 +const DEBUG_MODE = process.env.DEBUG_MODE === 'true'; + +/** + * 1) debugLog() + * 일반 디버그 로그 출력 (console.log) + * 개발 환경에서 API 흐름/변수 확인에 사용 + */ +export const debugLog = (...args: unknown[]) => { + if (DEBUG_MODE) { + console.log('[DEBUG]', ...args); + } +}; + +/** + * 2) debugError() + * 에러 상황을 디버그 모드에서만 출력 (console.error) + * error-handler.js 내부에서 활용 + */ +export const debugError = (...args: unknown[]) => { + if (DEBUG_MODE) { + console.error('[DEBUG ERROR]', ...args); + } +}; + +/** + * 3) debugWarn() + * 경고성 메시지 출력 (console.warn) + */ +export const debugWarn = (...args: unknown[]) => { + if (DEBUG_MODE) { + console.warn('[DEBUG WARN]', ...args); + } +}; + +/** + * 4) isDebugMode() + * 현재 디버그 모드인지 여부를 Boolean으로 반환 + */ +export const isDebugMode = () => DEBUG_MODE; + +/** + * 5) runInDebugMode(fn) + * 디버그 모드일 때만 특정 함수를 실행 + * @example + * runInDebugMode(() => console.log('개발 모드 전용 실행')); + */ +export const runInDebugMode = (fn: () => void) => { + if (DEBUG_MODE && typeof fn === 'function') { + fn(); + } +}; + +/** + * 6) startTimer(label) + * API 성능 측정용 (미들웨어에서 사용) + * @example + * const end = startTimer('DB 조회'); + * ... DB 작업 ... + * end(); + */ +export const startTimer = (label: string) => { + if (!DEBUG_MODE) return () => {}; // 배포에서는 완전 무효(no-op) + + const startTime = Date.now(); + return () => { + const endTime = Date.now(); + debugLog(`[타이머] ${label}: ${endTime - startTime}ms`); + }; +}; + +/** + * 7) debugObject(obj) + * 객체를 보기 좋게 출력 (depth 제한 없음 + 색상) + * 복잡한 객체 구조 디버깅할 때 유용 + */ +export const debugObject = (obj: unknown, label = 'Object') => { + if (DEBUG_MODE) { + console.log(`[DEBUG ${label}]`); + console.dir(obj, { depth: null, colors: true }); + } +}; + +/** + * 8) debugTable(data) + * 배열 또는 객체를 테이블 형태로 출력 + * 목록 확인 등에서 유용 + */ +export const debugTable = (data: unknown[] | Record, label = '') => { + if (DEBUG_MODE) { + if (label) console.log(`[DEBUG] ${label}`); + console.table(data); + } +}; diff --git a/src/core/error/error-handler.ts b/src/core/error/error-handler.ts new file mode 100644 index 00000000..a20d84dc --- /dev/null +++ b/src/core/error/error-handler.ts @@ -0,0 +1,161 @@ +// TODO) Error-Handler: AppError 기반 에러 핸들러 +// ?) 클래스 기반 에러 생성 + 전역 처리 로직 요약 설명 +import type { NextFunction, Request, Response } from 'express'; +import { isDebugMode } from './debug.js'; +import { logger } from './logger.js'; + +// 1) 공통 AppError 기반 클래스 정의 +export class AppError extends Error { + statusCode: number; + isOperational: boolean; + path: string | null; + + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + this.isOperational = true; + this.path = null; + + Error.captureStackTrace(this, this.constructor); + } +} + +// 2) 400 에러 클래스: 입력/검증 실패 +export class ValidationError extends AppError { + constructor(pathOrMessage: string | null, message?: string) { + if (message !== undefined) { + super(message, 400); + this.path = pathOrMessage; + } else { + super(pathOrMessage || '입력 데이터가 올바르지 않습니다', 400); + } + } +} + +// 3) 401 에러 클래스: 인증 실패/권한 없음 +export class UnauthorizedError extends AppError { + constructor(pathOrMessage: string | null, message?: string) { + if (message !== undefined) { + super(message, 401); + this.path = pathOrMessage; + } else { + super(pathOrMessage || '비밀번호가 일치하지 않습니다', 401); + } + } +} + +// 4) 403 에러 클래스: 권한 거부/접근 금지 +export class ForbiddenError extends AppError { + constructor(message = '권한이 없습니다') { + super(message, 403); + } +} + +// 5) 404 에러 클래스: 리소스 없음 +export class NotFoundError extends AppError { + constructor(message = '리소스를 찾을 수 없습니다') { + super(message, 404); + } +} + +// 6) 409 에러 클래스: 중복/충돌 +export class ConflictError extends AppError { + constructor(message = '이미 존재하는 데이터입니다') { + super(message, 409); + } +} + +// 7) 422 에러 클래스: 검증 실패/처리 불가 +export class UnprocessableEntityError extends AppError { + constructor(message = '요청 데이터를 처리할 수 없습니다') { + super(message, 422); + } +} + +// 8) 전역 에러 핸들러: 앱 전체 에러 응답 처리 +export const errorHandler = ( + err: any, + req: Request, + res: Response, + _next: NextFunction +) => { + const isProd = process.env.NODE_ENV === 'production'; + + // 8-A) winston 로깅 출력 + logger.error('⚠️ 글로벌 에러 발생', { + stack: err.stack, + path: req.path, + method: req.method, + user: req.user?.id, + code: err.code, + }); + + // 8-B) 디버그 모드에서만 추가 상세 로그 + if (isDebugMode()) { + logger.debug('⚠️ 글로벌 에러 상세', { error: err }); + } + + // 8-1) Prisma 에러 처리 + if (err.code === 'P2002') { + return res.status(409).json({ + message: '이미 존재하는 데이터입니다', + error: 'CONFLICT', + }); + } + + if (err.code === 'P2003') { + return res.status(409).json({ + message: '참조 중인 데이터가 있어 처리할 수 없습니다', + error: 'FOREIGN_KEY_CONSTRAINT', + }); + } + + if (err.code === 'P2025') { + return res.status(404).json({ + message: '리소스를 찾을 수 없습니다', + error: 'NOT_FOUND', + }); + } + + // 8-2) Multer 에러 처리 (파일 업로드) + if (err.name === 'MulterError') { + return res.status(400).json({ + message: err.message, + error: 'FILE_UPLOAD_ERROR', + }); + } + + // 8-3) 커스텀 에러 처리 + if (err.isOperational) { + const errorType = err.constructor.name.replace('Error', '').toUpperCase(); + const response: { message: string; error: string; path?: string | null } = { + message: err.message, + error: errorType, + }; + if (err.path) { + response.path = err.path; + } + return res.status(err.statusCode).json(response); + } + + // 8-4) 예상하지 못한 에러 처리 (기본값) + const statusCode = err.statusCode || 500; + const message = err.message || '서버 에러가 발생했습니다'; + const response: { message: string; error: string; stack?: string } = { + message, + error: 'SERVER_ERROR', + }; + if (!isProd && err.stack) { + response.stack = err.stack; + } + + res.status(statusCode).json(response); +}; + +// 9) 404 핸들러: 존재하지 않는 라우트 처리 +export const notFoundHandler = (_req: Request, res: Response) => { + res.status(404).json({ + message: '요청한 리소스를 찾을 수 없습니다', + error: 'NOT_FOUND', + }); +}; diff --git a/src/core/error/logger.ts b/src/core/error/logger.ts new file mode 100644 index 00000000..2e32c46c --- /dev/null +++ b/src/core/error/logger.ts @@ -0,0 +1,28 @@ +// TODO) Logger: Winston 기반 공용 로거 설정 +/** + * @see https://github.com/winstonjs/winston#readme + */ + +import { createLogger, format, transports } from 'winston'; + +const { combine, timestamp, printf, colorize, errors } = format; + +// 1) 개발/운영 공용 로거 (레벨은 환경변수로 제어) +export const logger = createLogger({ + level: process.env.LOG_LEVEL || 'info', + format: combine( + errors({ stack: true }), + timestamp(), + printf(({ level, message, timestamp: ts, stack, ...meta }) => { + const rest = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : ''; + return stack + ? `[${ts}] ${level}: ${message} ${rest}\n${stack}` + : `[${ts}] ${level}: ${message}${rest}`; + }) + ), + transports: [ + new transports.Console({ + format: combine(colorize(), timestamp(), errors({ stack: true })), + }), + ], +}); diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts new file mode 100644 index 00000000..e61374d2 --- /dev/null +++ b/src/middleware/auth.ts @@ -0,0 +1,44 @@ +// TODO) Auth: 요청마다 실행되는 커스텀 로직 +import type { NextFunction, Request, Response } from 'express'; +import { UnauthorizedError } from '../core/error/error-handler.js'; + +import { authService, type TokenPayload } from '../services/auth-service.js'; + +// 1) 토큰 payload 형태 +export type AuthUser = TokenPayload; + +// 2) 인증 미들웨어 함수 +export function requireAuth( + req: Request, + res: Response, + next: NextFunction +): void { + // 2-1) Authorization 헤더 파싱 + const auth = req.headers.authorization || ''; + const [type, token] = auth.split(' '); + + // 2-2) Bearer 토큰 검증 + if (type !== 'Bearer' || !token) { + return next( + new UnauthorizedError( + 'authorization', + '승인 헤더가 없거나 형식이 잘못되었습니다' + ) + ); + } + + // 2-3) 토큰 검증 & req.user 주입 + try { + const decoded = authService.verifyAccessToken(token) as TokenPayload; + req.user = { id: decoded.id, email: decoded.email }; + + return next(); + } catch (error) { + return next( + new UnauthorizedError( + 'authorization', + '유효하지 않거나 만료된 토큰입니다' + ) + ); + } +} diff --git a/src/repositories/article-comment-repository.ts b/src/repositories/article-comment-repository.ts new file mode 100644 index 00000000..ceb0e1cd --- /dev/null +++ b/src/repositories/article-comment-repository.ts @@ -0,0 +1,53 @@ +// TODO) Article-Comment-Repository: DB 저장소 +import type { Prisma } from '@prisma/client'; +import prisma from '../config/prisma.js'; + +export const articleCommentRepo = { + // 1) 댓글 목록 조회 + findByArticle(articleId: number) { + return prisma.articleComment.findMany({ + where: { articleId }, + orderBy: { createdAt: 'asc' }, + select: { + id: true, + content: true, + createdAt: true, + userId: true, + }, + }); + }, + + // 2) 특정 댓글 조회 + findById(id: number) { + return prisma.articleComment.findUnique({ + where: { id }, + select: { + id: true, + articleId: true, + content: true, + createdAt: true, + userId: true, + }, + }); + }, + + // 3) 댓글 생성 + create(data: Prisma.ArticleCommentUncheckedCreateInput) { + return prisma.articleComment.create({ data }); + }, + + // 4) 댓글 수정 + update(id: number, data: Prisma.ArticleCommentUpdateInput) { + return prisma.articleComment.update({ + where: { id }, + data, + }); + }, + + // 5) 댓글 삭제 + remove(id: number) { + return prisma.articleComment.delete({ + where: { id }, + }); + }, +}; diff --git a/src/repositories/article-like-repository.ts b/src/repositories/article-like-repository.ts new file mode 100644 index 00000000..a804ebec --- /dev/null +++ b/src/repositories/article-like-repository.ts @@ -0,0 +1,40 @@ +// TODO) Article-Like-Repository: 게시글 좋아요 저장소 +import prisma from '../config/prisma.js'; + +export const articleLikeRepo = { + // 1) 좋아요 조회 + findArticleLike(userId: number, articleId: number) { + return prisma.articleLike.findUnique({ + where: { userId_articleId: { userId, articleId } }, + }); + }, + + // 2) 좋아요 등록 + createArticleLike(userId: number, articleId: number) { + return prisma.articleLike.create({ data: { userId, articleId } }); + }, + + // 3) 좋아요 취소 + deleteArticleLike(userId: number, articleId: number) { + return prisma.articleLike.delete({ + where: { userId_articleId: { userId, articleId } }, + }); + }, + + // 4) 좋아요 목록 조회 + listLikedArticles(userId: number) { + return prisma.articleLike.findMany({ + where: { userId }, + select: { + article: { + select: { + id: true, + title: true, + content: true, + createdAt: true, + }, + }, + }, + }); + }, +}; diff --git a/src/repositories/article-repository.ts b/src/repositories/article-repository.ts new file mode 100644 index 00000000..e3ea2a37 --- /dev/null +++ b/src/repositories/article-repository.ts @@ -0,0 +1,51 @@ +// TODO) Article-Repository: DB 저장소 +import type { Prisma } from '@prisma/client'; +import prisma from '../config/prisma.js'; + +export const articleRepo = { + // 1) 게시글 목록 조회 + findArticles( + where: Prisma.ArticleWhereInput, + orderBy: Prisma.ArticleOrderByWithRelationInput, + offset: number, + limit: number + ) { + return prisma.article.findMany({ + where, + orderBy, + skip: offset, + take: limit, + select: { id: true, title: true, content: true, createdAt: true }, + }); + }, + + // 2) 게시글 단건 조회 + findArticleById(id: number) { + return prisma.article.findUnique({ + where: { id }, + select: { + id: true, + title: true, + content: true, + createdAt: true, + updatedAt: true, + userId: true, + }, + }); + }, + + // 3) 게시글 생성 + createArticle(data: Prisma.ArticleUncheckedCreateInput) { + return prisma.article.create({ data }); + }, + + // 4) 게시글 수정 + updateArticle(id: number, data: Prisma.ArticleUpdateInput) { + return prisma.article.update({ where: { id }, data }); + }, + + // 5) 게시글 삭제 + deleteArticle(id: number) { + return prisma.article.delete({ where: { id } }); + }, +}; diff --git a/src/repositories/notification-repository.ts b/src/repositories/notification-repository.ts new file mode 100644 index 00000000..410bb6b2 --- /dev/null +++ b/src/repositories/notification-repository.ts @@ -0,0 +1,79 @@ +// TODO) Notification-Repository: 알림 저장소 +import type { Prisma } from '@prisma/client'; +import prisma from '../config/prisma.js'; + +export const notificationRepo = { + // 1) 알림 목록 조회 + findByUser(userId: number) { + return prisma.notification.findMany({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + select: { + id: true, + type: true, + message: true, + isRead: true, + productId: true, + articleId: true, + createdAt: true, + }, + }); + }, + + // 2) 알림 단건 조회 + findById(id: number) { + return prisma.notification.findUnique({ + where: { id }, + select: { + id: true, + userId: true, + type: true, + message: true, + isRead: true, + productId: true, + articleId: true, + createdAt: true, + }, + }); + }, + + // 3) 미읽음 개수 조회 + countUnread(userId: number) { + return prisma.notification.count({ + where: { userId, isRead: false }, + }); + }, + + // 4) 알림 생성 + create(data: Prisma.NotificationUncheckedCreateInput) { + return prisma.notification.create({ + data, + select: { + id: true, + type: true, + message: true, + isRead: true, + productId: true, + articleId: true, + createdAt: true, + }, + }); + }, + + // 5) 읽음 처리 + markRead(id: number) { + return prisma.notification.update({ + where: { id }, + data: { isRead: true }, + select: { + id: true, + type: true, + message: true, + isRead: true, + productId: true, + articleId: true, + createdAt: true, + }, + }); + }, +}; diff --git a/src/repositories/product-comment-repository.ts b/src/repositories/product-comment-repository.ts new file mode 100644 index 00000000..633de8f8 --- /dev/null +++ b/src/repositories/product-comment-repository.ts @@ -0,0 +1,48 @@ +// TODO) Product-Comment-Repository: DB 저장소 +import type { Prisma } from '@prisma/client'; +import prisma from '../config/prisma.js'; + +export const productCommentRepo = { + // 1) 댓글 목록 조회 + findCommentsByProduct(productId: number) { + return prisma.productComment.findMany({ + where: { productId }, + orderBy: { createdAt: 'asc' }, + select: { + id: true, + content: true, + createdAt: true, + userId: true, + }, + }); + }, + + // 2) 특정 댓글 조회 + findProductCommentById(id: number) { + return prisma.productComment.findUnique({ + where: { id }, + select: { + id: true, + productId: true, + content: true, + createdAt: true, + userId: true, + }, + }); + }, + + // 3) 댓글 생성 + createProductComment(data: Prisma.ProductCommentUncheckedCreateInput) { + return prisma.productComment.create({ data }); + }, + + // 4) 댓글 수정 + updateProductComment(id: number, data: Prisma.ProductCommentUpdateInput) { + return prisma.productComment.update({ where: { id }, data }); + }, + + // 5) 댓글 삭제 + deleteProductComment(id: number) { + return prisma.productComment.delete({ where: { id } }); + }, +}; diff --git a/src/repositories/product-like-repository.ts b/src/repositories/product-like-repository.ts new file mode 100644 index 00000000..0e361753 --- /dev/null +++ b/src/repositories/product-like-repository.ts @@ -0,0 +1,49 @@ +// TODO) Product-Like-Repository: 상품 좋아요 저장소 +import prisma from '../config/prisma.js'; + +export const productLikeRepo = { + // 1) 좋아요 조회 + findProductLike(userId: number, productId: number) { + return prisma.productLike.findUnique({ + where: { userId_productId: { userId, productId } }, + }); + }, + + // 2) 좋아요 등록 + createProductLike(userId: number, productId: number) { + return prisma.productLike.create({ data: { userId, productId } }); + }, + + // 3) 좋아요 취소 + deleteProductLike(userId: number, productId: number) { + return prisma.productLike.delete({ + where: { userId_productId: { userId, productId } }, + }); + }, + + // 4) 좋아요 목록 조회 + listLikedProducts(userId: number) { + return prisma.productLike.findMany({ + where: { userId }, + select: { + product: { + select: { + id: true, + name: true, + price: true, + imagePath: true, + createdAt: true, + }, + }, + }, + }); + }, + + // 5) 상품 좋아요 유저 목록 조회 + listUserIdsByProduct(productId: number) { + return prisma.productLike.findMany({ + where: { productId }, + select: { userId: true }, + }); + }, +}; diff --git a/src/repositories/product-repository.ts b/src/repositories/product-repository.ts new file mode 100644 index 00000000..5278683e --- /dev/null +++ b/src/repositories/product-repository.ts @@ -0,0 +1,96 @@ +// TODO) Product-Repository: DB 저장소 +import type { Prisma } from '@prisma/client'; +import prisma from '../config/prisma.js'; + +export const productRepo = { + // 1) 상품 목록 조회 + findProducts( + where: Prisma.ProductWhereInput, + orderBy: Prisma.ProductOrderByWithRelationInput, + offset: number, + limit: number + ) { + return prisma.product.findMany({ + where, + orderBy, + skip: offset, + take: limit, + select: { + id: true, + name: true, + price: true, + stock: true, + createdAt: true, + imagePath: true, + }, + }); + }, + + // 2) 상품 단건 조회 + findProductById(id: number) { + return prisma.product.findUnique({ + where: { id }, + select: { + id: true, + name: true, + description: true, + price: true, + tags: true, + stock: true, + imagePath: true, + createdAt: true, + updatedAt: true, + userId: true, + }, + }); + }, + + // 3) 상품 생성 + createProduct(data: Prisma.ProductUncheckedCreateInput) { + return prisma.product.create({ data }); + }, + + // 4) 상품 수정 + updateProduct(id: number, data: Prisma.ProductUpdateInput) { + return prisma.product.update({ where: { id }, data }); + }, + + // 5) 상품 삭제 + deleteProduct(id: number) { + return prisma.product.delete({ where: { id } }); + }, + + // 6) 상품 구매 트랜잭션 + purchaseProductTx(productId: number, quantity: number, unitPrice: number) { + return prisma.$transaction(async (pr) => { + // 6-1) 재고 감소 + const updated = await pr.product.update({ + where: { id: productId }, + data: { stock: { decrement: quantity } }, + select: { id: true, name: true, stock: true }, + }); + + // 6-2) 구매 이력 + const purchase = await pr.purchase.create({ + data: { productId, quantity, unitPrice }, + }); + + return { updated, purchase }; + }); + }, + + // 7) 유저가 등록한 상품 조회 + findProductsByUser(userId: number) { + return prisma.product.findMany({ + where: { userId }, + select: { + id: true, + name: true, + price: true, + stock: true, + imagePath: true, + createdAt: true, + }, + }); + }, +}; diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts new file mode 100644 index 00000000..144fdd4c --- /dev/null +++ b/src/repositories/user-repository.ts @@ -0,0 +1,46 @@ +// TODO) User-Repository: DB 저장소 +import type { Prisma } from '@prisma/client'; +import prisma from '../config/prisma.js'; + +export const userRepo = { + // 1) 유저 생성 + createUser(data: Prisma.UserCreateInput) { + return prisma.user.create({ data }); + }, + + // 2) 이메일로 조회 + findUserByEmail(email: string) { + return prisma.user.findUnique({ where: { email } }); + }, + + // 3) 아이디로 조회 + findUserById(id: number) { + return prisma.user.findUnique({ where: { id } }); + }, + + // 4) 유저 수정 + updateUser(id: number, data: Prisma.UserUpdateInput) { + return prisma.user.update({ where: { id }, data }); + }, + + // 5) 유저 삭제 + deleteUser(id: number) { + return prisma.user.delete({ where: { id } }); + }, + + // 6) 토큰 저장 + setUserRefreshToken(userId: number, token: string | null) { + return prisma.user.update({ + where: { id: userId }, + data: { refreshToken: token }, + }); + }, + + // 7) 토큰 제거 + clearUserRefreshToken(userId: number) { + return prisma.user.update({ + where: { id: userId }, + data: { refreshToken: null }, + }); + }, +}; diff --git a/src/routes/article-comment-routes.ts b/src/routes/article-comment-routes.ts new file mode 100644 index 00000000..3690834e --- /dev/null +++ b/src/routes/article-comment-routes.ts @@ -0,0 +1,50 @@ +// TODO) Article-Comment-Routes: URL 매핑 +import { Router } from 'express'; +import asyncHandler from '../core/error/async-handler.js'; +import { requireAuth } from '../middleware/auth.js'; + +import validate, { validateParams } from '../validator/validate.js'; +import { + ArticleCommentIdParams, + ArticleCommentParams, + CreateArticleComment, + PatchArticleComment, +} from '../validator/article-comment-validator.js'; + +import { articleCommentController } from '../controllers/article-comment-controller.js'; + +const router = Router(); + +// 1) 댓글 목록 조회 +router.get( + '/:articleId', + validateParams(ArticleCommentParams), + asyncHandler(articleCommentController.list) +); + +// 2) 댓글 생성 +router.post( + '/', + requireAuth, + validate(CreateArticleComment), + asyncHandler(articleCommentController.create) +); + +// 3) 댓글 수정 +router.patch( + '/:id', + requireAuth, + validateParams(ArticleCommentIdParams), + validate(PatchArticleComment), + asyncHandler(articleCommentController.update) +); + +// 4) 댓글 삭제 +router.delete( + '/:id', + requireAuth, + validateParams(ArticleCommentIdParams), + asyncHandler(articleCommentController.remove) +); + +export default router; diff --git a/src/routes/article-like-routes.ts b/src/routes/article-like-routes.ts new file mode 100644 index 00000000..8dc3f223 --- /dev/null +++ b/src/routes/article-like-routes.ts @@ -0,0 +1,31 @@ +// TODO) Article-Like-Routes: URL 매핑 +import { Router } from 'express'; +import asyncHandler from '../core/error/async-handler.js'; +import { requireAuth } from '../middleware/auth.js'; +import { validateParams } from '../validator/validate.js'; +import { ArticleParams } from '../validator/article-validator.js'; + +import { articleLikeController } from '../controllers/article-like-controller.js'; + +const router = Router(); + +// 1) 좋아요한 게시글 조회 +router.get('/me/likes', requireAuth, asyncHandler(articleLikeController.list)); + +// 2) 게시글 좋아요 등록 +router.post( + '/:id/like', + requireAuth, + validateParams(ArticleParams), + asyncHandler(articleLikeController.like) +); + +// 3) 게시글 좋아요 취소 +router.delete( + '/:id/like', + requireAuth, + validateParams(ArticleParams), + asyncHandler(articleLikeController.unlike) +); + +export default router; diff --git a/src/routes/article-routes.ts b/src/routes/article-routes.ts new file mode 100644 index 00000000..ef9b8532 --- /dev/null +++ b/src/routes/article-routes.ts @@ -0,0 +1,52 @@ +// TODO) Article-Routes: URL 매핑 +import { Router } from 'express'; +import asyncHandler from '../core/error/async-handler.js'; +import { requireAuth } from '../middleware/auth.js'; + +import validate, { validateParams } from '../validator/validate.js'; +import { + ArticleParams, + CreateArticle, + PatchArticle, +} from '../validator/article-validator.js'; + +import { articleController } from '../controllers/article-controller.js'; + +const router = Router(); + +// 1) 게시글 목록 조회 +router.get('/', asyncHandler(articleController.list)); + +// 2) 게시글 조회 +router.get( + '/:id', + validateParams(ArticleParams), + asyncHandler(articleController.detail) +); + +// 3) 게시글 생성 +router.post( + '/', + requireAuth, + validate(CreateArticle), + asyncHandler(articleController.create) +); + +// 4) 게시글 수정 +router.patch( + '/:id', + requireAuth, + validateParams(ArticleParams), + validate(PatchArticle), + asyncHandler(articleController.update) +); + +// 5) 게시글 삭제 +router.delete( + '/:id', + requireAuth, + validateParams(ArticleParams), + asyncHandler(articleController.remove) +); + +export default router; diff --git a/src/routes/health-routes.ts b/src/routes/health-routes.ts new file mode 100644 index 00000000..a38c4c36 --- /dev/null +++ b/src/routes/health-routes.ts @@ -0,0 +1,17 @@ +// TODO) Health-Routes: URL 매핑 +import { Router } from 'express'; + +import { + checkHealth, + checkDatabase, +} from '../controllers/health-controller.js'; + +const router = Router(); + +// 1) 서버 연결 확인 +router.get('/', checkHealth); + +// 2) DB 연결 확인 +router.get('/db', checkDatabase); + +export default router; diff --git a/src/routes/notification-routes.ts b/src/routes/notification-routes.ts new file mode 100644 index 00000000..0d00d7b4 --- /dev/null +++ b/src/routes/notification-routes.ts @@ -0,0 +1,29 @@ +// TODO) Notification-Routes: URL 매핑 +import { Router } from 'express'; +import asyncHandler from '../core/error/async-handler.js'; +import { requireAuth } from '../middleware/auth.js'; +import { validateParams } from '../validator/validate.js'; +import { NotificationIdParams } from '../validator/notification-validator.js'; +import { notificationController } from '../controllers/notification-controller.js'; + +const router = Router(); + +// 1) 알림 목록 조회 +router.get('/', requireAuth, asyncHandler(notificationController.list)); + +// 2) 미읽음 개수 조회 +router.get( + '/unread-count', + requireAuth, + asyncHandler(notificationController.unreadCount) +); + +// 3) 읽음 처리 +router.patch( + '/:id/read', + requireAuth, + validateParams(NotificationIdParams), + asyncHandler(notificationController.markRead) +); + +export default router; diff --git a/src/routes/product-comment-routes.ts b/src/routes/product-comment-routes.ts new file mode 100644 index 00000000..9668f2f7 --- /dev/null +++ b/src/routes/product-comment-routes.ts @@ -0,0 +1,50 @@ +// TODO) Product-Comment-Routes: URL 매핑 +import { Router } from 'express'; +import asyncHandler from '../core/error/async-handler.js'; +import { requireAuth } from '../middleware/auth.js'; + +import validate, { validateParams } from '../validator/validate.js'; +import { + ProductCommentIdParams, + ProductCommentParams, + CreateProductComment, + PatchProductComment, +} from '../validator/product-comment-validator.js'; + +import { productCommentController } from '../controllers/product-comment-controller.js'; + +const router = Router(); + +// 1) 상품 댓글 목록 조회 +router.get( + '/:productId', + validateParams(ProductCommentParams), + asyncHandler(productCommentController.list) +); + +// 2) 상품 댓글 생성 +router.post( + '/', + requireAuth, + validate(CreateProductComment), + asyncHandler(productCommentController.create) +); + +// 3) 상품 댓글 수정 +router.patch( + '/:id', + requireAuth, + validateParams(ProductCommentIdParams), + validate(PatchProductComment), + asyncHandler(productCommentController.update) +); + +// 4) 상품 댓글 삭제 +router.delete( + '/:id', + requireAuth, + validateParams(ProductCommentIdParams), + asyncHandler(productCommentController.remove) +); + +export default router; diff --git a/src/routes/product-like-routes.ts b/src/routes/product-like-routes.ts new file mode 100644 index 00000000..2447cc17 --- /dev/null +++ b/src/routes/product-like-routes.ts @@ -0,0 +1,31 @@ +// TODO) Product-Like-Routes: URL 매핑 +import { Router } from 'express'; +import asyncHandler from '../core/error/async-handler.js'; +import { requireAuth } from '../middleware/auth.js'; +import { validateParams } from '../validator/validate.js'; +import { ProductParams } from '../validator/product-validator.js'; + +import { productLikeController } from '../controllers/product-like-controller.js'; + +const router = Router(); + +// 1) 좋아요한 상품 조회 +router.get('/me/likes', requireAuth, asyncHandler(productLikeController.list)); + +// 2) 상품 좋아요 등록 +router.post( + '/:id/like', + requireAuth, + validateParams(ProductParams), + asyncHandler(productLikeController.like) +); + +// 3) 상품 좋아요 취소 +router.delete( + '/:id/like', + requireAuth, + validateParams(ProductParams), + asyncHandler(productLikeController.unlike) +); + +export default router; diff --git a/src/routes/product-routes.ts b/src/routes/product-routes.ts new file mode 100644 index 00000000..5793b6ac --- /dev/null +++ b/src/routes/product-routes.ts @@ -0,0 +1,61 @@ +// TODO) Product-Routes: URL 매핑 +import { Router } from 'express'; +import asyncHandler from '../core/error/async-handler.js'; +import { requireAuth } from '../middleware/auth.js'; + +import validate, { validateParams } from '../validator/validate.js'; +import { + CreateProduct, + PatchProduct, + ProductParams, +} from '../validator/product-validator.js'; +import { PurchaseProduct } from '../validator/purchase-validator.js'; + +import { productController } from '../controllers/product-controller.js'; + +const router = Router(); + +// 1) 상품 목록 조회 +router.get('/', asyncHandler(productController.list)); + +// 2) 상품 조회 +router.get( + '/:id', + validateParams(ProductParams), + asyncHandler(productController.detail) +); + +// 3) 상품 생성 +router.post( + '/', + requireAuth, + validate(CreateProduct), + asyncHandler(productController.create) +); + +// 4) 상품 수정 +router.patch( + '/:id', + requireAuth, + validateParams(ProductParams), + validate(PatchProduct), + asyncHandler(productController.update) +); + +// 5) 상품 삭제 +router.delete( + '/:id', + requireAuth, + validateParams(ProductParams), + asyncHandler(productController.remove) +); + +// 6) 상품 구매 +router.post( + '/purchase', + requireAuth, + validate(PurchaseProduct), + asyncHandler(productController.purchase) +); + +export default router; diff --git a/src/routes/upload-routes.ts b/src/routes/upload-routes.ts new file mode 100644 index 00000000..0b9cd9f5 --- /dev/null +++ b/src/routes/upload-routes.ts @@ -0,0 +1,12 @@ +// TODO) Upload-Routes: URL 매핑 +import { Router } from 'express'; +import { upload } from '../config/multer.js'; + +import { uploadController } from '../controllers/upload-controller.js'; + +const router = Router(); + +// 1) 이미지 업로드 +router.post('/', upload.single('image'), uploadController.image); + +export default router; diff --git a/src/routes/user-routes.ts b/src/routes/user-routes.ts new file mode 100644 index 00000000..5640d4f5 --- /dev/null +++ b/src/routes/user-routes.ts @@ -0,0 +1,61 @@ +// TODO) User-Routes: URL 매핑 +import { Router } from 'express'; +import asyncHandler from '../core/error/async-handler.js'; +import { requireAuth } from '../middleware/auth.js'; + +import validate from '../validator/validate.js'; +import { + RegisterUser, + LoginUser, + UpdateProfile, +} from '../validator/user-validator.js'; + +import { userController } from '../controllers/user-controller.js'; + +const router = Router(); + +// 1) 회원가입 +router.post( + '/register', + validate(RegisterUser), + asyncHandler(userController.register) +); + +// 2) 로그인 +router.post('/login', validate(LoginUser), asyncHandler(userController.login)); + +// 3) 내 정보 조회 +router.get('/me', requireAuth, asyncHandler(userController.me)); + +// 4) 로그아웃 +router.post('/logout', requireAuth, asyncHandler(userController.logout)); + +// 5) 프로필 수정 +router.patch( + '/name', + requireAuth, + validate(UpdateProfile), + asyncHandler(userController.updateName) +); + +// 6) 비밀번호 변경 +router.patch( + '/password', + requireAuth, + asyncHandler(userController.updatePassword) +); + +// 7) 회원 탈퇴 +router.delete('/', requireAuth, asyncHandler(userController.removeAccount)); + +// 8) 내가 등록한 상품 조회 +router.get( + '/me/products', + requireAuth, + asyncHandler(userController.myProducts) +); + +// 9) 토큰 재발급 +router.post('/token/refresh', asyncHandler(userController.refresh)); + +export default router; diff --git a/src/services/article-comment-service.ts b/src/services/article-comment-service.ts new file mode 100644 index 00000000..e8053ef6 --- /dev/null +++ b/src/services/article-comment-service.ts @@ -0,0 +1,93 @@ +// TODO) Article-Comment-Service: 비즈니스 로직 처리 +import { NotFoundError, ForbiddenError } from '../core/error/error-handler.js'; +import { assertContent } from '../utils/to-content.js'; + +import { articleCommentRepo } from '../repositories/article-comment-repository.js'; +import { articleRepo } from '../repositories/article-repository.js'; +import { notificationService } from './notification-service.js'; + +export const articleCommentService = { + // 1) 댓글 목록 조회 + async list(articleId: number) { + // 1-1) 댓글 대상 게시글 조회 + const article = await articleRepo.findArticleById(articleId); + + // 1-2) 게시글 검증 + if (!article) { + throw new NotFoundError('게시글을 찾을 수 없습니다'); + } + + return articleCommentRepo.findByArticle(articleId); + }, + + // 2) 댓글 생성 + async create( + { articleId, content }: { articleId: number; content: string }, + userId: number + ) { + // 2-1) 타입 string 변환 + const body = assertContent(content); + + // 2-2) 댓글 대상 게시글 조회 + const article = await articleRepo.findArticleById(articleId); + + // 2-3) 게시글 검증 + if (!article) { + throw new NotFoundError('게시글을 찾을 수 없습니다'); + } + + const comment = await articleCommentRepo.create({ + articleId, + content: body, + userId, + }); + + await notificationService.create({ + userId: article.userId, + type: 'ARTICLE_COMMENTED', + message: `"${article.title}"에 새로운 댓글이 달렸습니다`, + articleId: article.id, + }); + + return comment; + }, + + // 3) 댓글 수정 + async update(id: number, content: string, userId: number) { + // 3-1) 타입 string 변환 + const body = assertContent(content); + + // 3-2) 기존 댓글 조회 + const exists = await articleCommentRepo.findById(id); + + // 3-3) 댓글 검증 + if (!exists) { + throw new NotFoundError('댓글을 찾을 수 없습니다'); + } + + // 3-4) 권한 검증 + if (exists.userId !== userId) { + throw new ForbiddenError('댓글 수정 권한이 없습니다.'); + } + + return articleCommentRepo.update(id, { content: body }); + }, + + // 4) 댓글 삭제 + async remove(id: number, userId: number) { + // 4-1) 기존 댓글 조회 + const exists = await articleCommentRepo.findById(id); + + // 4-2) 댓글 검증 + if (!exists) { + throw new NotFoundError('댓글을 찾을 수 없습니다'); + } + + // 4-3) 권한 검증 + if (exists.userId !== userId) { + throw new ForbiddenError('댓글 삭제 권한이 없습니다.'); + } + + return articleCommentRepo.remove(id); + }, +}; diff --git a/src/services/article-like-service.ts b/src/services/article-like-service.ts new file mode 100644 index 00000000..ca77c3e6 --- /dev/null +++ b/src/services/article-like-service.ts @@ -0,0 +1,50 @@ +// TODO) Article-Like-Service: 비즈니스 로직 처리 +import { NotFoundError, ConflictError } from '../core/error/error-handler.js'; + +import { articleRepo } from '../repositories/article-repository.js'; +import { articleLikeRepo } from '../repositories/article-like-repository.js'; + +export const articleLikeService = { + // 1) 게시글 좋아요 등록 + async like(userId: number, articleId: number) { + // 1-1) 좋아요 대상 게시글 조회 + const article = await articleRepo.findArticleById(articleId); + + // 1-2) 게시글 검증 + if (!article) { + throw new NotFoundError('게시글을 찾을 수 없습니다'); + } + + // 1-3) 기존 좋아요 여부 확인 + const existed = await articleLikeRepo.findArticleLike(userId, articleId); + + // 1-4) 좋아요 검증 + if (existed) { + throw new ConflictError('이미 좋아요한 게시글입니다'); + } + + await articleLikeRepo.createArticleLike(userId, articleId); + + return { articleId, liked: true }; + }, + + // 2) 게시글 좋아요 취소 + async unlike(userId: number, articleId: number) { + // 2-1) 기존 좋아요 여부 조회 + const like = await articleLikeRepo.findArticleLike(userId, articleId); + + // 2-2) 좋아요 검증 + if (!like) { + throw new NotFoundError('좋아요가 존재하지 않습니다'); + } + + await articleLikeRepo.deleteArticleLike(userId, articleId); + + return { articleId, liked: false }; + }, + + // 3) 좋아요한 게시글 조회 + list(userId: number) { + return articleLikeRepo.listLikedArticles(userId); + }, +}; diff --git a/src/services/article-service.ts b/src/services/article-service.ts new file mode 100644 index 00000000..543e6d4f --- /dev/null +++ b/src/services/article-service.ts @@ -0,0 +1,116 @@ +// TODO) Article-Service: 비즈니스 로직 처리 +import type { Prisma } from '@prisma/client'; +import { + NotFoundError, + ValidationError, + ForbiddenError, +} from '../core/error/error-handler.js'; +import { toIntOrThrow } from '../utils/to-int.js'; +import { + ARTICLE_ORDER, + ARTICLE_ORDER_MAP, + DEFAULT_ARTICLE_ORDER, +} from '../constants/article.js'; + +import { articleRepo } from '../repositories/article-repository.js'; +import { articleLikeRepo } from '../repositories/article-like-repository.js'; + +export const articleService = { + // 1) 게시글 목록 조회 + async list(query: Record) { + // 1-1) 쿼리 파라미터 기본값 분리 + const { offset = 0, limit = 10, order = DEFAULT_ARTICLE_ORDER, q } = query; + + // 1-2) offset 타입 number 변환 + const skip = toIntOrThrow(offset, 'offset'); + + // 1-3) limit 타입 number 변환 + const take = toIntOrThrow(limit, 'limit'); + + // 1-4) 정렬 키 타입 string 변환 + const orderKey = String( + order || DEFAULT_ARTICLE_ORDER + ).toLowerCase() as (typeof ARTICLE_ORDER)[keyof typeof ARTICLE_ORDER]; + + // 1-5) 정렬 매핑 조회 + const orderBy = ARTICLE_ORDER_MAP[orderKey]; + + // 1-6) 정렬 검증 + if (!orderBy) { + throw new ValidationError( + 'order', + `${Object.keys(ARTICLE_ORDER_MAP).join(', ')} 중 하나여야 합니다` + ); + } + + // 1-7) 검색 조건(where) 구성 + const where: Prisma.ArticleWhereInput = {}; + + // 1-8) 검색 조건 있을 시 조건 반환, 없으면 전체 목록 반환 + if (q) { + where.OR = [ + { title: { contains: String(q), mode: 'insensitive' as const } }, + { content: { contains: String(q), mode: 'insensitive' as const } }, + ]; + } + + return articleRepo.findArticles(where, orderBy, skip, take); + }, + + // 2) 게시글 조회 + async getOrThrow(id: number) { + // 2-1) 게시글 조회 + const article = await articleRepo.findArticleById(id); + + // 2-3) 게시글 검증 + if (!article) { + throw new NotFoundError('게시글을 찾을 수 없습니다'); + } + + return article; + }, + + // 3) 게시글 생성 + async create(data: Prisma.ArticleUncheckedCreateInput, userId: number) { + return await articleRepo.createArticle({ ...data, userId }); + }, + + // 4) 게시글 수정 + async update(id: number, data: Prisma.ArticleUpdateInput, userId: number) { + // 4-1) 게시글 조회 + const article = await articleRepo.findArticleById(id); + + // 4-3) 게시글 검증 + if (!article) throw new NotFoundError('게시글을 찾을 수 없습니다'); + + // 4-4) 권한 검증 + if (article.userId !== userId) { + throw new ForbiddenError('게시글 수정 권한이 없습니다.'); + } + + return articleRepo.updateArticle(id, data); + }, + + // 5) 게시글 삭제 + async remove(id: number, userId: number) { + // 5-1) 게시글 조회 + const article = await articleRepo.findArticleById(id); + + // 5-3) 게시글 검증 + if (!article) throw new NotFoundError('게시글을 찾을 수 없습니다'); + + // 5-4) 권한 검증 + if (article.userId !== userId) { + throw new ForbiddenError('게시글 삭제 권한이 없습니다.'); + } + + return articleRepo.deleteArticle(id); + }, + + // 6) 좋아요 여부 확인 + async isLiked(userId: number, articleId: number) { + const like = await articleLikeRepo.findArticleLike(userId, articleId); + + return Boolean(like); + }, +}; diff --git a/src/services/auth-service.ts b/src/services/auth-service.ts new file mode 100644 index 00000000..52a89501 --- /dev/null +++ b/src/services/auth-service.ts @@ -0,0 +1,93 @@ +// TODO) Auth-Service: 비즈니스 로직 처리 +import jwt, { type JwtPayload, type Secret } from 'jsonwebtoken'; + +/** + * @see https://github.com/vercel/ms + */ + +import type { StringValue } from 'ms'; +import { logger } from '../core/error/logger.js'; + +import { userRepo } from '../repositories/user-repository.js'; + +// 1) TokenPayload 정의 +export interface TokenPayload { + id: number; + email: string; +} + +// 2) 환경변수 검증 +const getRequiredEnv = (key: string): string => { + const value = process.env[key]; + if (!value) { + // 2-1) 누락 시 로그 + logger.error(`${key}: 환경 변수가 누락되었습니다`); + // 2-2) 누락 시 에러 + throw new Error('환경 변수 설정이 올바르지 않으니 확인하세요'); + } + return value; +}; + +// 3) JWT 설정 로드 +const ACCESS_SECRET: Secret = getRequiredEnv('JWT_ACCESS_SECRET'); +const REFRESH_SECRET: Secret = getRequiredEnv('JWT_REFRESH_SECRET'); +const ACCESS_EXPIRES_IN: StringValue | number = (process.env + .JWT_ACCESS_EXPIRES_IN ?? '1h') as StringValue; +const REFRESH_EXPIRES_IN: StringValue | number = (process.env + .JWT_REFRESH_EXPIRES_IN ?? '14d') as StringValue; + +export const authService = { + // 4) 액세스 토큰 발급 + signAccessToken(payload: TokenPayload): string { + return jwt.sign(payload, ACCESS_SECRET, { expiresIn: ACCESS_EXPIRES_IN }); + }, + + // 5) 리프레시 토큰 발급 + signRefreshToken(payload: TokenPayload): string { + return jwt.sign(payload, REFRESH_SECRET, { + expiresIn: REFRESH_EXPIRES_IN, + }); + }, + + // 6) 액세스 토큰 검증 + verifyAccessToken(token: string): TokenPayload { + return jwt.verify(token, ACCESS_SECRET) as TokenPayload; + }, + + // 7) 리프레시 토큰 검증 + verifyRefreshToken(token: string): TokenPayload & JwtPayload { + return jwt.verify(token, REFRESH_SECRET) as TokenPayload & JwtPayload; + }, + + // 8) 토큰 세트 발급 + async generateTokens(user: TokenPayload) { + const accessToken = this.signAccessToken({ + id: user.id, + email: user.email, + }); + + const refreshToken = this.signRefreshToken({ + id: user.id, + email: user.email, + }); + + await userRepo.setUserRefreshToken(user.id, refreshToken); + + return { accessToken, refreshToken }; + }, + + // 9) 액세스 토큰 재발급 + rotateAccessToken(refreshToken: string) { + const decoded = this.verifyRefreshToken(refreshToken); + + return this.signAccessToken({ + id: decoded.id, + email: decoded.email, + }); + }, + + // 10) 리프레시 토큰 제거 + clearRefreshToken(userId: number) { + return userRepo.clearUserRefreshToken(userId); + }, +}; diff --git a/src/services/notification-service.ts b/src/services/notification-service.ts new file mode 100644 index 00000000..aeec2536 --- /dev/null +++ b/src/services/notification-service.ts @@ -0,0 +1,51 @@ +// TODO) Notification-Service: 알림 비즈니스 로직 +import type { NotificationType } from '@prisma/client'; +import { NotFoundError } from '../core/error/error-handler.js'; +import { notificationRepo } from '../repositories/notification-repository.js'; +import { emitToUser, SOCKET_EVENT_NOTIFICATION } from '../socket/io.js'; + +type CreateNotificationPayload = { + userId: number; + type: NotificationType; + message: string; + productId?: number | null; + articleId?: number | null; +}; + +export const notificationService = { + // 1) 알림 목록 조회 + async list(userId: number) { + return notificationRepo.findByUser(userId); + }, + + // 2) 미읽음 개수 조회 + async countUnread(userId: number) { + return notificationRepo.countUnread(userId); + }, + + // 3) 읽음 처리 + async markRead(id: number, userId: number) { + const notification = await notificationRepo.findById(id); + + if (!notification || notification.userId !== userId) { + throw new NotFoundError('알림을 찾을 수 없습니다'); + } + + return notificationRepo.markRead(id); + }, + + // 4) 알림 생성 + 실시간 전송 + async create(payload: CreateNotificationPayload) { + const created = await notificationRepo.create({ + userId: payload.userId, + type: payload.type, + message: payload.message, + productId: payload.productId ?? null, + articleId: payload.articleId ?? null, + }); + + emitToUser(payload.userId, SOCKET_EVENT_NOTIFICATION, created); + + return created; + }, +}; diff --git a/src/services/product-comment-service.ts b/src/services/product-comment-service.ts new file mode 100644 index 00000000..bdec1128 --- /dev/null +++ b/src/services/product-comment-service.ts @@ -0,0 +1,85 @@ +// TODO) Product-Comment-Service: 비즈니스 로직 처리 +import { NotFoundError, ForbiddenError } from '../core/error/error-handler.js'; +import { assertContent } from '../utils/to-content.js'; + +import { productCommentRepo } from '../repositories/product-comment-repository.js'; +import { productRepo } from '../repositories/product-repository.js'; + +export const productCommentService = { + // 1) 댓글 목록 조회 + async list(productId: number) { + // 1-1) 댓글 대상 상품 조회 + const product = await productRepo.findProductById(productId); + + // 1-2) 상품 검증 + if (!product) { + throw new NotFoundError('상품을 찾을 수 없습니다'); + } + + return productCommentRepo.findCommentsByProduct(productId); + }, + + // 2) 댓글 생성 + async create( + { productId, content }: { productId: number; content: string }, + userId: number + ) { + // 2-1) 타입 string 변환 + const body = assertContent(content); + + // 2-2) 댓글 대상 상품 조회 + const product = await productRepo.findProductById(productId); + + // 2-3) 상품 검증 + if (!product) { + throw new NotFoundError('상품을 찾을 수 없습니다'); + } + + return productCommentRepo.createProductComment({ + productId, + content: body, + userId, + }); + }, + + // 3) 댓글 수정 + async update(id: number, content: string, userId: number) { + // 3-1) 타입 string 변환 + const body = assertContent(content); + + // 3-2) 기존 댓글 조회 + const exists = await productCommentRepo.findProductCommentById(id); + + // 3-3) 댓글 검증 + if (!exists) { + throw new NotFoundError('댓글을 찾을 수 없습니다'); + } + + // 3-4) 권한 검증 + if (exists.userId !== userId) { + throw new ForbiddenError('댓글 수정 권한이 없습니다.'); + } + + return productCommentRepo.updateProductComment(id, { + content: body, + }); + }, + + // 4) 댓글 삭제 + async remove(id: number, userId: number) { + // 4-1) 기존 댓글 조회 + const exists = await productCommentRepo.findProductCommentById(id); + + // 4-2) 댓글 검증 + if (!exists) { + throw new NotFoundError('댓글을 찾을 수 없습니다'); + } + + // 4-3) 권한 검증 + if (exists.userId !== userId) { + throw new ForbiddenError('댓글 삭제 권한이 없습니다.'); + } + + return productCommentRepo.deleteProductComment(id); + }, +}; diff --git a/src/services/product-like-service.ts b/src/services/product-like-service.ts new file mode 100644 index 00000000..89a014fb --- /dev/null +++ b/src/services/product-like-service.ts @@ -0,0 +1,50 @@ +// TODO) Product-Like-Service: 비즈니스 로직 처리 +import { NotFoundError, ConflictError } from '../core/error/error-handler.js'; + +import { productRepo } from '../repositories/product-repository.js'; +import { productLikeRepo } from '../repositories/product-like-repository.js'; + +export const productLikeService = { + // 1) 상품 좋아요 등록 + async like(userId: number, productId: number) { + // 1-1) 좋아요 대상 상품 조회 + const product = await productRepo.findProductById(productId); + + // 1-2) 상품 검증 + if (!product) { + throw new NotFoundError('상품을 찾을 수 없습니다'); + } + + // 1-3) 기존 좋아요 여부 확인 + const existed = await productLikeRepo.findProductLike(userId, productId); + + // 1-4) 좋아요 검증 + if (existed) { + throw new ConflictError('이미 좋아요한 상품입니다'); + } + + await productLikeRepo.createProductLike(userId, productId); + + return { productId, liked: true }; + }, + + // 2) 상품 좋아요 취소 + async unlike(userId: number, productId: number) { + // 1-1) 기존 좋아요 여부 조회 + const like = await productLikeRepo.findProductLike(userId, productId); + + // 1-2) 좋아요 검증 + if (!like) { + throw new NotFoundError('좋아요가 존재하지 않습니다'); + } + + await productLikeRepo.deleteProductLike(userId, productId); + + return { productId, liked: false }; + }, + + // 3) 좋아요한 상품 조회 + async list(userId: number) { + return await productLikeRepo.listLikedProducts(userId); + }, +}; diff --git a/src/services/product-service.ts b/src/services/product-service.ts new file mode 100644 index 00000000..729dc300 --- /dev/null +++ b/src/services/product-service.ts @@ -0,0 +1,174 @@ +// TODO) Product-Service: 비즈니스 로직 처리 +import type { Prisma, Tag } from '@prisma/client'; +import { + NotFoundError, + ValidationError, + ForbiddenError, + UnprocessableEntityError, +} from '../core/error/error-handler.js'; +import { toIntOrThrow } from '../utils/to-int.js'; +import { + PRODUCT_ORDER, + PRODUCT_ORDER_MAP, + DEFAULT_PRODUCT_ORDER, +} from '../constants/product.js'; + +import { productRepo } from '../repositories/product-repository.js'; +import { productLikeRepo } from '../repositories/product-like-repository.js'; +import { notificationService } from './notification-service.js'; + +export const productService = { + // 1) 상품 목록 조회 + async list(query: Record) { + // 1-1) 쿼리 파라미터 기본값 분리 + const { offset = 0, limit = 10, order = 'recent', q, tag } = query; + + // 1-2) offset 타입 number 변환 + const skip = toIntOrThrow(offset, 'offset'); + + // 1-3) limit 타입 number 변환 + const take = toIntOrThrow(limit, 'limit'); + + // 1-4) 정렬 키 타입 string 변환 + const orderKey = String( + order || DEFAULT_PRODUCT_ORDER + ).toLowerCase() as (typeof PRODUCT_ORDER)[keyof typeof PRODUCT_ORDER]; + + // 1-5) 정렬 매핑 조회 + const orderBy = PRODUCT_ORDER_MAP[orderKey]; + + // 1-6) 정렬 검증 + if (!orderBy) { + throw new ValidationError( + 'order', + `${Object.keys(PRODUCT_ORDER_MAP).join(', ')} 중 하나여야 합니다` + ); + } + + // 1-7) 검색 조건(where) 구성 + const where: Prisma.ProductWhereInput = {}; + + // 1-8) 검색 조건 있을 시 조건 반환, 없으면 전체 목록 반환 + if (q) { + where.OR = [ + { name: { contains: String(q), mode: 'insensitive' as const } }, + { description: { contains: String(q), mode: 'insensitive' as const } }, + ]; + } + + // 1-9) 태그 필터 조건 추가 + if (tag) { + where.tags = String(tag) as Tag; + } + + return productRepo.findProducts(where, orderBy, skip, take); + }, + + // 2) 상품 조회 + async getOrThrow(id: number) { + // 2-1) 상품 조회 + const product = await productRepo.findProductById(id); + + // 2-3) 상품 검증 + if (!product) { + throw new NotFoundError('상품을 찾을 수 없습니다'); + } + + return product; + }, + + // 3) 상품 생성 + async create(data: Prisma.ProductUncheckedCreateInput, userId: number) { + return await productRepo.createProduct({ ...data, userId }); + }, + + // 4) 상품 수정 + async update(id: number, data: Prisma.ProductUpdateInput, userId: number) { + // 4-1) 상품 조회 + const product = await productRepo.findProductById(id); + + // 4-3) 상품 검증 + if (!product) throw new NotFoundError('상품을 찾을 수 없습니다'); + + // 4-4) 권한 검증 + if (product.userId !== userId) { + throw new ForbiddenError('상품 수정 권한이 없습니다'); + } + + const nextPrice = + typeof data.price === 'number' + ? data.price + : typeof data.price === 'object' && data.price && 'set' in data.price + ? data.price.set + : undefined; + + const priceChanged = + typeof nextPrice === 'number' && nextPrice !== product.price; + + const updated = await productRepo.updateProduct(id, data); + + if (priceChanged) { + const likedUsers = await productLikeRepo.listUserIdsByProduct(id); + const targets = likedUsers.map((item) => item.userId); + + await Promise.all( + targets.map((targetUserId) => + notificationService.create({ + userId: targetUserId, + type: 'PRODUCT_PRICE_CHANGED', + message: `"${product.name}"의 가격이 ${product.price}원에서 ${nextPrice}원으로 변경되었습니다`, + productId: product.id, + }) + ) + ); + } + + return updated; + }, + + // 5) 상품 삭제 + async remove(id: number, userId: number) { + // 5-1) 상품 조회 + const product = await productRepo.findProductById(id); + + // 5-3) 상품 검증 + if (!product) throw new NotFoundError('상품을 찾을 수 없습니다'); + + // 5-4) 권한 검증 + if (product.userId !== userId) { + throw new ForbiddenError('상품 삭제 권한이 없습니다'); + } + + return productRepo.deleteProduct(id); + }, + + // 6) 상품 구매 + async purchase(productId: number, quantity: number) { + // 6-1) 상품 조회 + const product = await productRepo.findProductById(productId); + + // 6-2) 상품 검증 + if (!product) { + throw new NotFoundError('상품을 찾을 수 없습니다'); + } + + // 6-3) 재고 검증 + if (quantity > product.stock) { + throw new UnprocessableEntityError('재고가 충분하지 않습니다'); + } + + return productRepo.purchaseProductTx(product.id, quantity, product.price); + }, + + // 7) 좋아요 여부 확인 + async isLiked(userId: number, productId: number) { + const like = await productLikeRepo.findProductLike(userId, productId); + + return Boolean(like); + }, + + // 8) 유저별 상품 목록 조회 + async listByUser(userId: number) { + return await productRepo.findProductsByUser(userId); + }, +}; diff --git a/src/services/user-service.ts b/src/services/user-service.ts new file mode 100644 index 00000000..625527b4 --- /dev/null +++ b/src/services/user-service.ts @@ -0,0 +1,142 @@ +// TODO) User-Service: 비즈니스 로직 처리 +import type { Prisma } from '@prisma/client'; +import { + ConflictError, + UnauthorizedError, + NotFoundError, +} from '../core/error/error-handler.js'; +import { hashPassword, verifyPassword } from '../utils/to-hash.js'; + +import { userRepo } from '../repositories/user-repository.js'; + +// 1) 회원가입 타입 정의 +type RegisterInput = { + email: string; + password: string; + nickname: string; + image?: string | null; +}; + +export const userService = { + // 1) 회원 가입 + async registerUser({ + email, + password, + nickname, + image = null, + }: RegisterInput) { + // 1-1) 이메일 조회 + const exists = await userRepo.findUserByEmail(email); + + // 1-2) 이메일 검증 + if (exists) { + throw new ConflictError('이미 존재하는 email입니다'); + } + + // 1-3) 비번 해쉬화 + const hashed = await hashPassword(password); + + return userRepo.createUser({ email, password: hashed, nickname, image }); + }, + + // 2) 로그인 + async loginUser(email: string, password: string) { + // 2-1) 이메일 조회 + const user = await userRepo.findUserByEmail(email); + + // 2-2) 이메일 검증 + if (!user) { + throw new UnauthorizedError('이메일 또는 비밀번호가 유효하지 않습니다'); + } + + // 2-3) 비번 조회 + const ok = await verifyPassword(password, user.password); + + // 2-4) 비번 검증 + if (!ok) { + throw new UnauthorizedError('이메일 또는 비밀번호가 유효하지 않습니다'); + } + + return user; + }, + + // 3) 내 정보 조회 + async getMe(userId: number) { + // 3-1) 가존 유저 조회 + const user = await userRepo.findUserById(userId); + + // 3-2) 유저 검증 + if (!user) { + throw new NotFoundError('유저를 찾을 수 없습니다'); + } + + // 3-3) 민감 정보 제거 + const { password, refreshToken, ...safeUser } = user; + + return safeUser; + }, + + // 4) 프로필 수정 + async changeProfile( + userId: number, + { nickname, image }: { nickname?: string; image?: string | null } + ) { + // 4-1) 기존 유저 조회 + const user = await userRepo.findUserById(userId); + + // 4-2) 유저 검증 + if (!user) throw new NotFoundError('유저를 찾을 수 없습니다'); + + // 4-3) 부분 업데이트용 데이터 객체 + const data: Prisma.UserUpdateInput = {}; + + // 4-4) 부분 업데이트용 데이터 반영 + if (nickname !== undefined) data.nickname = nickname; + if (image !== undefined) data.image = image; + + // 4-5) 새로운 프로필 저장 + const updated = await userRepo.updateUser(userId, data); + + // 4-6) 민감 정보 제거 + const { password, refreshToken, ...safeUser } = updated; + + return safeUser; + }, + + // 5) 비밀번호 변경 + async changePassword(userId: number, oldPw: string, newPw: string) { + // 5-1) 기존 유저 조회 + const user = await userRepo.findUserById(userId); + + // 5-2) 유저 검증 + if (!user) { + throw new NotFoundError('유저를 찾을 수 없습니다'); + } + + // 5-3) 기존 비번 조회 + const ok = await verifyPassword(oldPw, user.password); + + // 5-4) 비번 검증 + if (!ok) { + throw new UnauthorizedError('기존 비밀번호가 일치하지 않습니다'); + } + + // 5-5) 새로운 비번 해쉬화 + const hashed = await hashPassword(newPw); + + return userRepo.updateUser(userId, { password: hashed }); + }, + + // 6) 회원 탈퇴 + async deleteAccount(userId: number) { + // 6-1) 기존 유저 조회 + const user = await userRepo.findUserById(userId); + + // 6-2) 유저 검증 + if (!user) { + throw new NotFoundError('유저를 찾을 수 없습니다'); + } + + return userRepo.deleteUser(userId); + }, +}; diff --git a/src/socket/io.ts b/src/socket/io.ts new file mode 100644 index 00000000..d28deb92 --- /dev/null +++ b/src/socket/io.ts @@ -0,0 +1,84 @@ +// TODO) Socket.IO: 실시간 알림용 서버 +import type { Server as HttpServer } from 'http'; +import { Server } from 'socket.io'; +import { authService, type TokenPayload } from '../services/auth-service.js'; + +let io: Server | null = null; + +export const SOCKET_EVENT_NOTIFICATION = 'notification'; + +function toOrigin(value: string): string | null { + const trimmed = value.trim(); + if (!trimmed) return null; + + try { + return new URL(trimmed).origin; + } catch { + return null; + } +} + +function getAllowedOrigins(): string[] { + const configured = (process.env.SOCKET_CORS_ORIGINS ?? '') + .split(',') + .map((value) => toOrigin(value)) + .filter((value): value is string => Boolean(value)); + + if (configured.length > 0) { + return Array.from(new Set(configured)); + } + + const defaults = [ + toOrigin(process.env.BASE_URL ?? ''), + toOrigin('http://localhost:3000'), + toOrigin('http://localhost:5173'), + ].filter((value): value is string => Boolean(value)); + + return Array.from(new Set(defaults)); +} + +export function initSocket(server: HttpServer) { + const allowedOrigins = getAllowedOrigins(); + + io = new Server(server, { + cors: { + origin: allowedOrigins, + credentials: true, + }, + }); + + io.use((socket, next) => { + const header = socket.handshake.headers.authorization; + const bearerToken = + typeof header === 'string' ? header.split(' ')[1] : null; + const token = socket.handshake.auth?.token || bearerToken; + + if (!token) { + return next(new Error('Unauthorized')); + } + + try { + const decoded = authService.verifyAccessToken(token) as TokenPayload; + socket.data.userId = decoded.id; + return next(); + } catch { + return next(new Error('Unauthorized')); + } + }); + + io.on('connection', (socket) => { + const userId = socket.data.userId as number | undefined; + if (userId) { + socket.join(`user:${userId}`); + } + }); + + return io; +} + +export function emitToUser(userId: number, event: string, payload: T) { + if (!io) { + throw new Error('Socket.IO server is not initialized'); + } + io.to(`user:${userId}`).emit(event, payload); +} diff --git a/src/test/jest.setup.cjs b/src/test/jest.setup.cjs new file mode 100644 index 00000000..981bf6b8 --- /dev/null +++ b/src/test/jest.setup.cjs @@ -0,0 +1,10 @@ +process.env.NODE_ENV = 'test'; +process.env.DEBUG_MODE = 'false'; +process.env.LOG_LEVEL = 'error'; + +if (!process.env.JWT_ACCESS_SECRET) { + process.env.JWT_ACCESS_SECRET = 'test-access-secret'; +} +if (!process.env.JWT_REFRESH_SECRET) { + process.env.JWT_REFRESH_SECRET = 'test-refresh-secret'; +} diff --git a/src/test/socket-test.html b/src/test/socket-test.html new file mode 100644 index 00000000..a996ea57 --- /dev/null +++ b/src/test/socket-test.html @@ -0,0 +1,108 @@ + + + + + + Socket.IO 알림 테스트 + + + + +

알림 테스트

+

액세스 토큰을 입력하고 연결 버튼을 누르세요.

+
+ + + + +
+

+ Live Server 자동 리로드를 피하려면 파일을 직접 열거나 로컬 서버를 + 사용하세요. +

+
+ + +
+

+
+    
+  
+
diff --git a/src/test/test-helpers.ts b/src/test/test-helpers.ts
new file mode 100644
index 00000000..6cb8be7d
--- /dev/null
+++ b/src/test/test-helpers.ts
@@ -0,0 +1,30 @@
+import prisma from '../config/prisma.js';
+import { authService } from '../services/auth-service.js';
+import { userService } from '../services/user-service.js';
+
+export async function resetDb() {
+  await prisma.notification.deleteMany();
+  await prisma.productLike.deleteMany();
+  await prisma.articleLike.deleteMany();
+  await prisma.productComment.deleteMany();
+  await prisma.articleComment.deleteMany();
+  await prisma.purchase.deleteMany();
+  await prisma.product.deleteMany();
+  await prisma.article.deleteMany();
+  await prisma.user.deleteMany();
+}
+
+export async function createUserWithToken(email: string) {
+  const user = await userService.registerUser({
+    email,
+    password: 'password1234',
+    nickname: 'tester',
+    image: null,
+  });
+  const accessToken = authService.signAccessToken({
+    id: user.id,
+    email: user.email,
+  });
+
+  return { user, accessToken };
+}
diff --git a/src/test/test.http b/src/test/test.http
new file mode 100644
index 00000000..f71c63a4
--- /dev/null
+++ b/src/test/test.http
@@ -0,0 +1,90 @@
+@baseUrl = http://localhost:4000
+@accessToken = YOUR_ACCESS_TOKEN
+@refreshToken = YOUR_REFRESH_TOKEN
+
+### 회원가입
+POST {{baseUrl}}/users/register
+Content-Type: application/json
+
+{
+  "email": "user1@example.com",
+  "password": "user1234",
+  "nickname": "panda",
+  "image": null
+}
+
+### 로그인
+POST {{baseUrl}}/users/login
+Content-Type: application/json
+
+{
+  "email": "user1@example.com",
+  "password": "user1234"
+}
+
+### 토큰 재발급 (accessToken 발급)
+POST {{baseUrl}}/users/token/refresh
+Content-Type: application/json
+
+{
+  "refreshToken": "{{refreshToken}}"
+}
+
+### 알림 목록 조회
+GET {{baseUrl}}/notifications
+Authorization: Bearer {{accessToken}}
+
+### 미읽음 알림 개수 조회
+GET {{baseUrl}}/notifications/unread-count
+Authorization: Bearer {{accessToken}}
+
+### 알림 읽음 처리
+PATCH {{baseUrl}}/notifications/1/read
+Authorization: Bearer {{accessToken}}
+
+### 상품 생성
+POST {{baseUrl}}/products
+Authorization: Bearer {{accessToken}}
+Content-Type: application/json
+
+{
+  "name": "테스트 상품",
+  "description": "소켓 알림 테스트용 상품",
+  "price": 12000,
+  "stock": 111,
+  "tags": "NONE",
+  "imagePath": "test.png"
+}
+
+### 게시글 생성
+POST {{baseUrl}}/articles
+Authorization: Bearer {{accessToken}}
+Content-Type: application/json
+
+{
+  "title": "알림 테스트 게시글",
+  "content": "댓글 알림 테스트용 글입니다"
+}
+
+### 상품 좋아요 등록 (가격 변경 알림 대상)
+POST {{baseUrl}}/product-likes/1/like
+Authorization: Bearer {{accessToken}}
+
+### 상품 가격 변경(알림 트리거)
+PATCH {{baseUrl}}/products/1
+Authorization: Bearer {{accessToken}}
+Content-Type: application/json
+
+{
+  "price": 12345
+}
+
+### 게시글 댓글 생성(알림 트리거)
+POST {{baseUrl}}/article-comments
+Authorization: Bearer {{accessToken}}
+Content-Type: application/json
+
+{
+  "articleId": 1,
+  "content": "테스트 댓글입니다"
+}
diff --git a/src/types/express.d.ts b/src/types/express.d.ts
new file mode 100644
index 00000000..5b154ab9
--- /dev/null
+++ b/src/types/express.d.ts
@@ -0,0 +1,12 @@
+// TODO) Express-Type-Extension: req.user 전역 선언
+import type { AuthUser } from '../middleware/auth.js';
+
+declare global {
+  namespace Express {
+    interface Request {
+      user?: AuthUser;
+    }
+  }
+}
+
+export {};
diff --git a/src/utils/to-content.ts b/src/utils/to-content.ts
new file mode 100644
index 00000000..5dc732f7
--- /dev/null
+++ b/src/utils/to-content.ts
@@ -0,0 +1,9 @@
+// TODO) To-Content: 문자열 검증 유틸
+import { ValidationError } from '../core/error/error-handler.js';
+
+export const assertContent = (content: unknown) => {
+  if (!content || typeof content !== 'string' || content.trim().length === 0) {
+    throw new ValidationError('content', '댓글 내용이 없습니다');
+  }
+  return content.trim();
+};
diff --git a/src/utils/to-hash.ts b/src/utils/to-hash.ts
new file mode 100644
index 00000000..e29c7974
--- /dev/null
+++ b/src/utils/to-hash.ts
@@ -0,0 +1,15 @@
+// TODO) To-Hash: 비밀번호 해시/검증 유틸
+import bcrypt from 'bcrypt';
+
+// 1) 해쉬 강도
+const SALT_ROUNDS = 10;
+
+// 2) 비번 해쉬화
+export async function hashPassword(password: string) {
+  return bcrypt.hash(password, SALT_ROUNDS);
+}
+
+// 3) 해쉬화 검증
+export async function verifyPassword(plain: string, hashed: string) {
+  return bcrypt.compare(plain, hashed);
+}
diff --git a/src/utils/to-int.ts b/src/utils/to-int.ts
new file mode 100644
index 00000000..734994e5
--- /dev/null
+++ b/src/utils/to-int.ts
@@ -0,0 +1,12 @@
+// TODO) To-Int: 숫자 검증 유틸
+import { ValidationError } from '../core/error/error-handler.js';
+
+export const toIntOrThrow = (value: unknown, field: string) => {
+  const num = Number(value);
+
+  if (!Number.isInteger(num)) {
+    throw new ValidationError(field, `정수가 아닌 ${field}입니다`);
+  }
+
+  return num;
+};
diff --git a/src/utils/upload-cleaner.ts b/src/utils/upload-cleaner.ts
new file mode 100644
index 00000000..e21a2334
--- /dev/null
+++ b/src/utils/upload-cleaner.ts
@@ -0,0 +1,51 @@
+// TODO) Upload-Cleaner: 이미지 업로드 정리
+import { unlink } from 'fs/promises';
+import path from 'path';
+import { logger } from '../core/error/logger.js';
+
+// 1) 업로드 경로 구분
+const UPLOAD_PREFIX = '/uploads/';
+
+// 2) 업로드 경로 함수 (절대/상대 경로 모두 지원 & 변한 실패 시 null 반환)
+const resolveUploadPath = (url: string | null | undefined): string | null => {
+  // 2-1) URL 없으면 null 반환
+  if (!url) return null;
+  try {
+    // 2-2) 업로드 Prefix가 포함되었는지 확인 (없으면 null 반환)
+    const idx = url.indexOf(UPLOAD_PREFIX);
+    if (idx === -1) return null;
+
+    // 2-3) Prefix 이후 상대 경로 추출 (없으면 null 반환)
+    const relative = url.slice(idx + UPLOAD_PREFIX.length);
+    if (!relative) return null;
+
+    // 2-4) 프로젝트 루트 절대 경로 반환
+    return path.join(process.cwd(), 'public', 'uploads', relative);
+  } catch {
+    // 2-5) 위 조건이 무시되면 null 반환 안전장치
+    return null;
+  }
+};
+
+// 3) 업로드 삭제 함수 (로컬에 저장된 파일 삭제 & 없을 시 조용히 무시)
+export const deleteLocalUploadIfExists = async (
+  url: string | null | undefined
+): Promise => {
+  // 3-1) URL 실제 경로로 반환
+  const filePath = resolveUploadPath(url);
+  if (!filePath) return;
+  try {
+    // 3-2) 파일 존재 시 삭제 시도
+    await unlink(filePath);
+  } catch (error: any) {
+    // 3-3) ENOENT(파일 없음)은 무시, 그 외 에러만 처리
+    if (error.code !== 'ENOENT') {
+      logger.error('파일 삭제 실패', {
+        message: error.message,
+        code: error.code,
+        stack: error.stack,
+        filePath,
+      });
+    }
+  }
+};
diff --git a/src/validator/article-comment-validator.ts b/src/validator/article-comment-validator.ts
new file mode 100644
index 00000000..dbde3285
--- /dev/null
+++ b/src/validator/article-comment-validator.ts
@@ -0,0 +1,27 @@
+// TODO) Article-Comment-Validator: 유효성 검사
+import * as s from 'superstruct';
+
+const IdFromParams = s.coerce(s.integer(), s.string(), Number);
+
+// 1) 댓글 본문 길이 제한
+const Content = s.size(s.string(), 1, 100);
+
+// 1-1) path params 스키마
+export const ArticleCommentParams = s.object({
+  articleId: IdFromParams,
+});
+
+export const ArticleCommentIdParams = s.object({
+  id: IdFromParams,
+});
+
+// 2) 댓글 생성 스키마 정의
+export const CreateArticleComment = s.object({
+  articleId: s.integer(),
+  content: Content,
+});
+
+// 3) 댓글 수정 스키마 정의
+export const PatchArticleComment = s.object({
+  content: Content,
+});
diff --git a/src/validator/article-validator.ts b/src/validator/article-validator.ts
new file mode 100644
index 00000000..15dee9b1
--- /dev/null
+++ b/src/validator/article-validator.ts
@@ -0,0 +1,18 @@
+// TODO) Article-Validator: 유효성 검사
+import * as s from 'superstruct';
+
+const IdFromParams = s.coerce(s.integer(), s.string(), Number);
+
+// 1) 게시글 생성 스키마 정의
+export const CreateArticle = s.object({
+  title: s.size(s.string(), 1, 30),
+  content: s.size(s.string(), 1, 300),
+});
+
+// 4) 게시글 수정 스키마 정의
+export const PatchArticle = s.partial(CreateArticle);
+
+// 5) path params 스키마 정의
+export const ArticleParams = s.object({
+  id: IdFromParams,
+});
diff --git a/src/validator/notification-validator.ts b/src/validator/notification-validator.ts
new file mode 100644
index 00000000..6029a4e8
--- /dev/null
+++ b/src/validator/notification-validator.ts
@@ -0,0 +1,9 @@
+// TODO) Notification-Validator: 유효성 검사
+import * as s from 'superstruct';
+
+const IdFromParams = s.coerce(s.integer(), s.string(), Number);
+
+// 1) path params 스키마
+export const NotificationIdParams = s.object({
+  id: IdFromParams,
+});
diff --git a/src/validator/product-comment-validator.ts b/src/validator/product-comment-validator.ts
new file mode 100644
index 00000000..80999a1c
--- /dev/null
+++ b/src/validator/product-comment-validator.ts
@@ -0,0 +1,27 @@
+// TODO) Product-Comment-Validator: 유효성 검사
+import * as s from 'superstruct';
+
+const IdFromParams = s.coerce(s.integer(), s.string(), Number);
+
+// 1) 댓글 본문 길이 제한
+const Content = s.size(s.string(), 1, 100);
+
+// 1-1) path params 스키마
+export const ProductCommentParams = s.object({
+  productId: IdFromParams,
+});
+
+export const ProductCommentIdParams = s.object({
+  id: IdFromParams,
+});
+
+// 2) 댓글 생성 스키마 정의
+export const CreateProductComment = s.object({
+  productId: s.integer(),
+  content: Content,
+});
+
+// 3) 댓글 수정 스키마 정의
+export const PatchProductComment = s.object({
+  content: Content,
+});
diff --git a/src/validator/product-validator.ts b/src/validator/product-validator.ts
new file mode 100644
index 00000000..6bb55c78
--- /dev/null
+++ b/src/validator/product-validator.ts
@@ -0,0 +1,35 @@
+// TODO) Product-Validator: 유효성 검사
+import * as s from 'superstruct';
+import { Tag } from '@prisma/client';
+
+const IdFromParams = s.coerce(s.integer(), s.string(), Number);
+
+// 1) enum에서 허용 태그 목록 추출
+const TAGS = Object.values(Tag);
+
+// 2) 이미지 확장자 검증 (jpg/jpeg/png만 허용)
+const validExt = ['.jpg', '.jpeg', '.png'];
+
+const ImagePath = s.optional(
+  s.refine(s.string(), 'ImagePathExt', (v: string) =>
+    validExt.some((ext) => v.toLowerCase().endsWith(ext))
+  )
+);
+
+// 3) 상품 생성 스키마 정의
+export const CreateProduct = s.object({
+  name: s.size(s.string(), 1, 20),
+  description: s.optional(s.string()),
+  price: s.min(s.number(), 0),
+  stock: s.min(s.integer(), 0),
+  tags: s.enums(TAGS),
+  imagePath: ImagePath,
+});
+
+// 4) 상품 수정 스키마 정의
+export const PatchProduct = s.partial(CreateProduct);
+
+// 5) path params 스키마 정의
+export const ProductParams = s.object({
+  id: IdFromParams,
+});
diff --git a/src/validator/purchase-validator.ts b/src/validator/purchase-validator.ts
new file mode 100644
index 00000000..c87367a9
--- /dev/null
+++ b/src/validator/purchase-validator.ts
@@ -0,0 +1,8 @@
+// TODO) Purchase-Validator: 유효성 검사
+import * as s from 'superstruct';
+
+// 1) 상품 구매 스키마 정의
+export const PurchaseProduct = s.object({
+  productId: s.min(s.integer(), 1),
+  quantity: s.min(s.integer(), 1),
+});
diff --git a/src/validator/user-validator.ts b/src/validator/user-validator.ts
new file mode 100644
index 00000000..0b723504
--- /dev/null
+++ b/src/validator/user-validator.ts
@@ -0,0 +1,49 @@
+// TODO) User-Validator: 유효성 검사
+import * as s from 'superstruct';
+
+// 1) 이메일 스키마 정의
+const Email = s.refine(s.string(), 'emailPattern', (v: string) => {
+  const trimmed = v.trim();
+  if (!trimmed.includes('@')) return false;
+
+  const [local, domain] = trimmed.split('@');
+  return Boolean(local && domain && domain?.includes('.'));
+});
+
+// 2) 패스워드 스키마 정의
+const Password = s.size(s.string(), 8, 64);
+
+// 3) 닉네임 스키마 정의
+const Nickname = s.size(s.string(), 1, 30);
+
+// 4) 프로필 사진 스키마 정의
+const validExt = ['.jpg', '.jpeg', '.png'];
+
+const Image = s.optional(
+  s.union([
+    s.refine(s.string(), 'imagePathExt', (v: string) =>
+      validExt.some((ext) => v.toLowerCase().endsWith(ext))
+    ),
+    s.literal(null),
+  ])
+);
+
+// 5) 회원가입 스키마 정의
+export const RegisterUser = s.object({
+  email: Email,
+  password: Password,
+  nickname: Nickname,
+  image: Image,
+});
+
+// 6) 로그인 스키마 정의
+export const LoginUser = s.object({
+  email: Email,
+  password: Password,
+});
+
+// 7) 프로필 사진 수정 스키마 정의
+export const UpdateProfile = s.object({
+  nickname: s.optional(Nickname),
+  image: Image,
+});
diff --git a/src/validator/validate.ts b/src/validator/validate.ts
new file mode 100644
index 00000000..48d96658
--- /dev/null
+++ b/src/validator/validate.ts
@@ -0,0 +1,121 @@
+// TODO) Validate: 공통 유효성 검사
+// ?) Superstruct 기반 공통 바디 검증 미들웨어
+import type { NextFunction, Request, Response } from 'express';
+import { create, type Failure, type Struct, StructError } from 'superstruct';
+import { ValidationError } from '../core/error/error-handler.js';
+
+type PayloadKey = 'body' | 'params' | 'query';
+
+// 1) 공통 스키마 검증 미들웨어 팩토리
+const validatePayload =
+  (schema: Struct, payloadKey: PayloadKey) =>
+  (req: Request, _res: Response, next: NextFunction) => {
+    try {
+      // 1-1) 요청 데이터 스키마 검증 및 변환 결과 재할당
+      const validated = create(req[payloadKey as keyof Request], schema);
+      (req as any)[payloadKey] = validated;
+
+      next();
+    } catch (error) {
+      // 1-2) Superstruct 에러만 추출
+      const structError = error instanceof StructError ? error : null;
+
+      // 1-3) 실패 리스트 배열로 정규화
+      const failures = structError
+        ? Array.from(structError.failures())
+        : ([] as Failure[]);
+
+      // 1-4) 첫 번째 실패 항목만 추출
+      const first = failures[0];
+
+      // 1-5) 실패 빌드 경로를 점 표기법으로 병합
+      const path =
+        first?.path?.join('.') || structError?.path?.join('.') || null;
+
+      // 1-6) 커스텀메시지 -> 기본 메시지 -> fallback
+      const message =
+        resolveCustomMessage(path, first) ||
+        first?.message ||
+        '요청 데이터가 올바르지 않습니다';
+
+      throw new ValidationError(path, message);
+    }
+  };
+
+// 2) 바디 검증 (기존 기본값)
+const validate = (schema: Struct) => validatePayload(schema, 'body');
+
+// 3) params/query 검증
+export const validateParams = (schema: Struct) =>
+  validatePayload(schema, 'params');
+export const validateQuery = (schema: Struct) =>
+  validatePayload(schema, 'query');
+
+export default validate;
+
+// 4) 필드/타입별 커스텀 메시지
+const resolveCustomMessage = (
+  path: string | null,
+  failure?: Failure
+): string | null => {
+  // 4-1) 실패 경로가 없으면 메시지 생성 불가
+  if (!path) return null;
+
+  // 4-2) 필드명별 기본 에러 메시지
+  const map: Record = {
+    email: '이메일 형식이 올바르지 않습니다',
+    password: '비밀번호는 8~64자여야 합니다',
+    nickname: '닉네임은 1~30자여야 합니다',
+    image: '이미지 확장자는 jpg, jpeg, png만 허용됩니다',
+    name: '상품명은 1~20자여야 합니다',
+    price: '가격은 0 이상이어야 합니다',
+    stock: '재고는 0 이상의 정수여야 합니다',
+    tags: 'tags는 NONE, FASHION, BEAUTY, SPORTS, ELECTRONICS, HOME_INTERIOR, HOUSEHOLD_SUPPLIES, KITCHENWARE 중 하나여야 합니다',
+    title: '제목은 1~30자여야 합니다',
+    content: '내용은 1~100자 이내여야 합니다',
+    productId: 'productId는 1 이상의 정수여야 합니다',
+    articleId: 'articleId는 1 이상의 정수여야 합니다',
+    quantity: 'quantity는 1 이상의 정수여야 합니다',
+  };
+
+  // 2-3) 사전에 매핑된 필드면 해당 메시지 반환
+  if (map[path]) return map[path];
+
+  // 2-4) 확장자 커스텀 실패 메시지 처리
+  if (
+    failure?.type === 'imagePathExt' ||
+    failure?.refinement === 'imagePathExt'
+  ) {
+    return 'jpg, jpeg, png 확장자여야 합니다';
+  }
+
+  // 2-5) 타입별 기본 에러 메시지
+  const typeMap: Record = {
+    string: 'Stirng',
+    number: 'Number',
+    integer: 'Integer',
+    boolean: 'Boolean',
+    array: 'Array',
+    object: 'Object',
+  };
+
+  // 2-6) 타입/범위/열거형 등 세부 실패 유형별 메시지 (확장 가능)
+  if (failure?.type && typeMap[failure.type]) {
+    return `${path}는 ${typeMap[failure.type]}이어야 합니다`;
+  }
+  if (failure?.type === 'min') {
+    return `${path}는 최소값보다 커야 합니다`;
+  }
+  if (failure?.type === 'max') {
+    return `${path}는 최대값을 초과할 수 없습니다`;
+  }
+  if (failure?.type === 'size') {
+    return `${path}의 길이가 제한을 초과하거나 부족합니다`;
+  }
+  if (failure?.type === 'enum' || failure?.type === 'enums') {
+    return `${path} 값이 허용된 목록에 없습니다`;
+  }
+
+  // 2-7) 매핑 실패 시 기본 메시지로 위임
+  return null;
+};
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..3c849b8f
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,38 @@
+{
+  "compilerOptions": {
+    /* === 📂 파일 구조 === */
+    "rootDir": "./src",
+    "outDir": "./dist",
+
+    /* === ⚙️ 실행 환경 설정 === */
+    "target": "ES2022",
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext",
+    "esModuleInterop": true,
+    "resolveJsonModule": true,
+    "allowSyntheticDefaultImports": true,
+
+    /* === 🧩 타입 관련 옵션 === */
+    "strict": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true,
+    "typeRoots": ["./src/types", "./node_modules/@types"],
+    "types": ["node", "jest"],
+
+    /* === 🏗️ 빌드 성능 & 개발 편의 === */
+    "incremental": true,
+    "tsBuildInfoFile": "./node_modules/.cache/.tsbuildinfo",
+    "isolatedModules": true,
+
+    /* ===  📝 빌드 Output 설정 === */
+    "sourceMap": true,
+    "declaration": true,
+    "declarationMap": true,
+
+    /* ===  📜 엄격한 타입 검사 옵션 === */
+    "noUncheckedIndexedAccess": true,
+    "exactOptionalPropertyTypes": true
+  },
+  "include": ["src/**/*.ts"],
+  "exclude": ["prisma", "node_modules", "dist"]
+}