From 4da46b5b8e39f15c5887d77f1ce2e92f42eb3645 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sat, 27 Dec 2025 01:31:10 +0700 Subject: [PATCH] Sure! Pl --- .cursor/plans/enterprise_f5e39e08.plan.md | 1323 +++ .cursor/skills/comment-code/SKILL.md | 572 ++ .cursor/skills/project-rules/SKILL.md | 476 + .github/workflows/ci-auth-service.yml | 73 + .github/workflows/ci-mobile.yml | 57 + .github/workflows/ci-web.yml | 49 + .github/workflows/deploy-production.yml | 58 + .github/workflows/deploy-staging.yml | 57 + .github/workflows/docker-build.yml | 87 + .github/workflows/pr-checks.yml | 34 + .gitignore | 82 + CONTRIBUTING.md | 93 + LICENSE | 21 + README.md | 200 + SETUP_GUIDE.md | 265 + apps/app-admin/.gitignore | 44 + apps/app-admin/Dockerfile | 20 + apps/app-admin/README.md | 43 + apps/app-admin/lib/main.dart | 71 + apps/app-admin/pubspec.yaml | 24 + apps/app-client/Dockerfile | 20 + apps/app-client/README.md | 43 + apps/app-client/lib/main.dart | 71 + apps/app-client/pubspec.yaml | 24 + apps/web-admin/Dockerfile | 50 + apps/web-admin/README.md | 31 + apps/web-admin/next-env.d.ts | 5 + apps/web-admin/next.config.js | 10 + apps/web-admin/package.json | 35 + apps/web-admin/postcss.config.js | 6 + apps/web-admin/public/.gitkeep | 1 + apps/web-admin/src/app/globals.css | 27 + apps/web-admin/src/app/layout.tsx | 19 + apps/web-admin/src/app/login/page.tsx | 60 + apps/web-admin/src/app/page.tsx | 34 + apps/web-admin/src/services/api/auth.api.ts | 45 + apps/web-admin/src/services/api/client.ts | 8 + apps/web-admin/src/stores/auth.store.ts | 103 + apps/web-admin/tailwind.config.js | 12 + apps/web-admin/tsconfig.json | 21 + apps/web-client/Dockerfile | 50 + apps/web-client/README.md | 31 + apps/web-client/next-env.d.ts | 5 + apps/web-client/next.config.js | 10 + apps/web-client/package.json | 35 + apps/web-client/postcss.config.js | 6 + apps/web-client/public/.gitkeep | 1 + apps/web-client/src/app/globals.css | 27 + apps/web-client/src/app/layout.tsx | 19 + apps/web-client/src/app/login/page.tsx | 60 + apps/web-client/src/app/page.tsx | 34 + apps/web-client/src/services/api/auth.api.ts | 45 + apps/web-client/src/services/api/client.ts | 8 + apps/web-client/src/stores/auth.store.ts | 103 + apps/web-client/tailwind.config.js | 12 + apps/web-client/tsconfig.json | 21 + deployments/local/README.md | 81 + deployments/local/docker-compose.yml | 120 + deployments/local/env.local.example | 54 + deployments/production/env.production.example | 19 + .../production/kubernetes/auth-service.yaml | 91 + .../production/kubernetes/configmap.yaml | 15 + .../production/kubernetes/ingress.yaml | 32 + .../kubernetes/secrets.yaml.example | 34 + deployments/staging/env.staging.example | 18 + .../staging/kubernetes/auth-service.yaml | 69 + deployments/staging/kubernetes/configmap.yaml | 14 + deployments/staging/kubernetes/ingress.yaml | 27 + .../staging/kubernetes/secrets.yaml.example | 34 + docs/README.md | 45 + docs/en/api/openapi/auth-service.yaml | 102 + docs/en/architecture/service-communication.md | 58 + docs/en/architecture/system-design.md | 81 + docs/en/guides/deployment.md | 106 + docs/en/guides/development.md | 87 + docs/en/guides/getting-started.md | 81 + docs/en/guides/local-development.md | 476 + docs/en/guides/neon-database.md | 215 + docs/en/guides/troubleshooting.md | 57 + docs/en/onboarding/new-developer-guide.md | 89 + docs/en/runbooks/incident-response.md | 65 + docs/en/runbooks/rollback-procedure.md | 71 + docs/vi/api/openapi/auth-service.yaml | 102 + docs/vi/architecture/service-communication.md | 58 + docs/vi/architecture/system-design.md | 81 + docs/vi/guides/deployment.md | 106 + docs/vi/guides/development.md | 87 + docs/vi/guides/getting-started.md | 81 + docs/vi/guides/local-development.md | 741 ++ docs/vi/guides/neon-database.md | 215 + docs/vi/guides/troubleshooting.md | 57 + docs/vi/onboarding/new-developer-guide.md | 89 + docs/vi/runbooks/incident-response.md | 65 + docs/vi/runbooks/rollback-procedure.md | 71 + infra/databases/docker-compose.databases.yml | 48 + infra/databases/neon/README.md | 125 + infra/databases/neon/setup.sh | 64 + infra/databases/postgres/init.sql | 8 + infra/databases/postgres/postgres.conf | 15 + infra/databases/redis/redis.conf | 21 + infra/docker/docker-compose.dev.yml | 9 + infra/docker/docker-compose.prod.yml | 12 + .../docker-compose.observability.yml | 58 + .../grafana/dashboards/service-overview.json | 39 + .../grafana/datasources/prometheus.yml | 15 + infra/observability/loki/loki-config.yml | 41 + infra/observability/prometheus/prometheus.yml | 29 + .../prometheus/rules/service-alerts.yml | 30 + infra/secrets/.gitignore | 4 + infra/traefik/README.md | 25 + infra/traefik/dynamic/middlewares.yml | 38 + infra/traefik/dynamic/routes.yml | 55 + infra/traefik/traefik.yml | 26 + package.json | 33 + packages/auth-sdk/README.md | 22 + packages/auth-sdk/package.json | 22 + packages/auth-sdk/src/index.ts | 68 + packages/auth-sdk/tsconfig.json | 9 + packages/config/eslint-config/index.js | 37 + packages/config/eslint-config/package.json | 13 + packages/config/prettier-config/index.js | 10 + packages/config/prettier-config/package.json | 9 + packages/config/tsconfig/base.json | 22 + packages/config/tsconfig/nextjs.json | 23 + packages/config/tsconfig/node.json | 9 + packages/config/tsconfig/package.json | 7 + packages/http-client/README.md | 26 + packages/http-client/package.json | 21 + packages/http-client/src/index.ts | 110 + packages/http-client/tsconfig.json | 9 + packages/logger/README.md | 29 + packages/logger/package.json | 20 + packages/logger/src/index.ts | 71 + packages/logger/tsconfig.json | 17 + packages/tracing/README.md | 16 + packages/tracing/package.json | 28 + packages/tracing/src/index.ts | 37 + packages/tracing/tsconfig.json | 9 + packages/types/README.md | 18 + packages/types/package.json | 16 + packages/types/src/api.types.ts | 29 + packages/types/src/auth.types.ts | 44 + packages/types/src/index.ts | 3 + packages/types/src/user.types.ts | 35 + packages/types/tsconfig.json | 17 + pnpm-lock.yaml | 8464 +++++++++++++++++ pnpm-workspace.yaml | 5 + scripts/db/backup.sh | 61 + scripts/db/migrate.sh | 51 + scripts/db/seed.sh | 36 + scripts/db/setup-neon.sh | 69 + scripts/deploy/deploy-prod.sh | 23 + scripts/deploy/deploy-staging.sh | 15 + scripts/dev/logs.sh | 26 + scripts/dev/setup-env.sh | 102 + scripts/dev/start-all.sh | 41 + scripts/dev/start-service.sh | 22 + scripts/setup/init-project.sh | 58 + scripts/setup/install-deps.sh | 9 + scripts/utils/cleanup.sh | 23 + scripts/utils/create-service.sh | 45 + services/_template/Dockerfile | 32 + services/_template/README.md | 38 + services/_template/package.json | 46 + services/_template/src/config/app.config.ts | 6 + .../_template/src/config/database.config.ts | 21 + services/_template/src/main.ts | 81 + .../src/middlewares/error.middleware.ts | 42 + .../src/middlewares/logger.middleware.ts | 19 + .../src/modules/feature/feature.controller.ts | 32 + .../src/modules/feature/feature.dto.ts | 9 + .../src/modules/feature/feature.module.ts | 11 + .../src/modules/feature/feature.service.ts | 10 + .../src/modules/health/health.controller.ts | 46 + services/_template/src/routes/index.ts | 20 + services/_template/tsconfig.json | 13 + services/auth-service/Dockerfile | 51 + services/auth-service/README.md | 131 + services/auth-service/env.local.example | 49 + services/auth-service/package.json | 54 + services/auth-service/prisma/schema.prisma | 58 + services/auth-service/prisma/seed.d.ts | 2 + services/auth-service/prisma/seed.d.ts.map | 1 + services/auth-service/prisma/seed.js | 31 + services/auth-service/prisma/seed.js.map | 1 + services/auth-service/prisma/seed.ts | 30 + .../auth-service/src/config/app.config.ts | 6 + .../src/config/database.config.ts | 21 + .../auth-service/src/config/jwt.config.ts | 10 + services/auth-service/src/main.ts | 81 + .../src/middlewares/auth.middleware.ts | 86 + .../src/middlewares/error.middleware.ts | 42 + .../src/middlewares/logger.middleware.ts | 19 + .../src/modules/auth/auth.controller.ts | 142 + .../auth-service/src/modules/auth/auth.dto.ts | 43 + .../src/modules/auth/auth.module.ts | 16 + .../src/modules/auth/auth.service.ts | 206 + .../src/modules/health/health.controller.ts | 46 + .../src/modules/user/user.controller.ts | 134 + .../auth-service/src/modules/user/user.dto.ts | 9 + .../src/modules/user/user.module.ts | 16 + .../src/modules/user/user.service.ts | 104 + services/auth-service/src/routes/index.ts | 22 + services/auth-service/tsconfig.json | 13 + turbo.json | 27 + 205 files changed, 21063 insertions(+) create mode 100644 .cursor/plans/enterprise_f5e39e08.plan.md create mode 100644 .cursor/skills/comment-code/SKILL.md create mode 100644 .cursor/skills/project-rules/SKILL.md create mode 100644 .github/workflows/ci-auth-service.yml create mode 100644 .github/workflows/ci-mobile.yml create mode 100644 .github/workflows/ci-web.yml create mode 100644 .github/workflows/deploy-production.yml create mode 100644 .github/workflows/deploy-staging.yml create mode 100644 .github/workflows/docker-build.yml create mode 100644 .github/workflows/pr-checks.yml create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SETUP_GUIDE.md create mode 100644 apps/app-admin/.gitignore create mode 100644 apps/app-admin/Dockerfile create mode 100644 apps/app-admin/README.md create mode 100644 apps/app-admin/lib/main.dart create mode 100644 apps/app-admin/pubspec.yaml create mode 100644 apps/app-client/Dockerfile create mode 100644 apps/app-client/README.md create mode 100644 apps/app-client/lib/main.dart create mode 100644 apps/app-client/pubspec.yaml create mode 100644 apps/web-admin/Dockerfile create mode 100644 apps/web-admin/README.md create mode 100644 apps/web-admin/next-env.d.ts create mode 100644 apps/web-admin/next.config.js create mode 100644 apps/web-admin/package.json create mode 100644 apps/web-admin/postcss.config.js create mode 100644 apps/web-admin/public/.gitkeep create mode 100644 apps/web-admin/src/app/globals.css create mode 100644 apps/web-admin/src/app/layout.tsx create mode 100644 apps/web-admin/src/app/login/page.tsx create mode 100644 apps/web-admin/src/app/page.tsx create mode 100644 apps/web-admin/src/services/api/auth.api.ts create mode 100644 apps/web-admin/src/services/api/client.ts create mode 100644 apps/web-admin/src/stores/auth.store.ts create mode 100644 apps/web-admin/tailwind.config.js create mode 100644 apps/web-admin/tsconfig.json create mode 100644 apps/web-client/Dockerfile create mode 100644 apps/web-client/README.md create mode 100644 apps/web-client/next-env.d.ts create mode 100644 apps/web-client/next.config.js create mode 100644 apps/web-client/package.json create mode 100644 apps/web-client/postcss.config.js create mode 100644 apps/web-client/public/.gitkeep create mode 100644 apps/web-client/src/app/globals.css create mode 100644 apps/web-client/src/app/layout.tsx create mode 100644 apps/web-client/src/app/login/page.tsx create mode 100644 apps/web-client/src/app/page.tsx create mode 100644 apps/web-client/src/services/api/auth.api.ts create mode 100644 apps/web-client/src/services/api/client.ts create mode 100644 apps/web-client/src/stores/auth.store.ts create mode 100644 apps/web-client/tailwind.config.js create mode 100644 apps/web-client/tsconfig.json create mode 100644 deployments/local/README.md create mode 100644 deployments/local/docker-compose.yml create mode 100644 deployments/local/env.local.example create mode 100644 deployments/production/env.production.example create mode 100644 deployments/production/kubernetes/auth-service.yaml create mode 100644 deployments/production/kubernetes/configmap.yaml create mode 100644 deployments/production/kubernetes/ingress.yaml create mode 100644 deployments/production/kubernetes/secrets.yaml.example create mode 100644 deployments/staging/env.staging.example create mode 100644 deployments/staging/kubernetes/auth-service.yaml create mode 100644 deployments/staging/kubernetes/configmap.yaml create mode 100644 deployments/staging/kubernetes/ingress.yaml create mode 100644 deployments/staging/kubernetes/secrets.yaml.example create mode 100644 docs/README.md create mode 100644 docs/en/api/openapi/auth-service.yaml create mode 100644 docs/en/architecture/service-communication.md create mode 100644 docs/en/architecture/system-design.md create mode 100644 docs/en/guides/deployment.md create mode 100644 docs/en/guides/development.md create mode 100644 docs/en/guides/getting-started.md create mode 100644 docs/en/guides/local-development.md create mode 100644 docs/en/guides/neon-database.md create mode 100644 docs/en/guides/troubleshooting.md create mode 100644 docs/en/onboarding/new-developer-guide.md create mode 100644 docs/en/runbooks/incident-response.md create mode 100644 docs/en/runbooks/rollback-procedure.md create mode 100644 docs/vi/api/openapi/auth-service.yaml create mode 100644 docs/vi/architecture/service-communication.md create mode 100644 docs/vi/architecture/system-design.md create mode 100644 docs/vi/guides/deployment.md create mode 100644 docs/vi/guides/development.md create mode 100644 docs/vi/guides/getting-started.md create mode 100644 docs/vi/guides/local-development.md create mode 100644 docs/vi/guides/neon-database.md create mode 100644 docs/vi/guides/troubleshooting.md create mode 100644 docs/vi/onboarding/new-developer-guide.md create mode 100644 docs/vi/runbooks/incident-response.md create mode 100644 docs/vi/runbooks/rollback-procedure.md create mode 100644 infra/databases/docker-compose.databases.yml create mode 100644 infra/databases/neon/README.md create mode 100755 infra/databases/neon/setup.sh create mode 100644 infra/databases/postgres/init.sql create mode 100644 infra/databases/postgres/postgres.conf create mode 100644 infra/databases/redis/redis.conf create mode 100644 infra/docker/docker-compose.dev.yml create mode 100644 infra/docker/docker-compose.prod.yml create mode 100644 infra/observability/docker-compose.observability.yml create mode 100644 infra/observability/grafana/dashboards/service-overview.json create mode 100644 infra/observability/grafana/datasources/prometheus.yml create mode 100644 infra/observability/loki/loki-config.yml create mode 100644 infra/observability/prometheus/prometheus.yml create mode 100644 infra/observability/prometheus/rules/service-alerts.yml create mode 100644 infra/secrets/.gitignore create mode 100644 infra/traefik/README.md create mode 100644 infra/traefik/dynamic/middlewares.yml create mode 100644 infra/traefik/dynamic/routes.yml create mode 100644 infra/traefik/traefik.yml create mode 100644 package.json create mode 100644 packages/auth-sdk/README.md create mode 100644 packages/auth-sdk/package.json create mode 100644 packages/auth-sdk/src/index.ts create mode 100644 packages/auth-sdk/tsconfig.json create mode 100644 packages/config/eslint-config/index.js create mode 100644 packages/config/eslint-config/package.json create mode 100644 packages/config/prettier-config/index.js create mode 100644 packages/config/prettier-config/package.json create mode 100644 packages/config/tsconfig/base.json create mode 100644 packages/config/tsconfig/nextjs.json create mode 100644 packages/config/tsconfig/node.json create mode 100644 packages/config/tsconfig/package.json create mode 100644 packages/http-client/README.md create mode 100644 packages/http-client/package.json create mode 100644 packages/http-client/src/index.ts create mode 100644 packages/http-client/tsconfig.json create mode 100644 packages/logger/README.md create mode 100644 packages/logger/package.json create mode 100644 packages/logger/src/index.ts create mode 100644 packages/logger/tsconfig.json create mode 100644 packages/tracing/README.md create mode 100644 packages/tracing/package.json create mode 100644 packages/tracing/src/index.ts create mode 100644 packages/tracing/tsconfig.json create mode 100644 packages/types/README.md create mode 100644 packages/types/package.json create mode 100644 packages/types/src/api.types.ts create mode 100644 packages/types/src/auth.types.ts create mode 100644 packages/types/src/index.ts create mode 100644 packages/types/src/user.types.ts create mode 100644 packages/types/tsconfig.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100755 scripts/db/backup.sh create mode 100755 scripts/db/migrate.sh create mode 100755 scripts/db/seed.sh create mode 100755 scripts/db/setup-neon.sh create mode 100755 scripts/deploy/deploy-prod.sh create mode 100755 scripts/deploy/deploy-staging.sh create mode 100755 scripts/dev/logs.sh create mode 100755 scripts/dev/setup-env.sh create mode 100755 scripts/dev/start-all.sh create mode 100755 scripts/dev/start-service.sh create mode 100755 scripts/setup/init-project.sh create mode 100755 scripts/setup/install-deps.sh create mode 100755 scripts/utils/cleanup.sh create mode 100755 scripts/utils/create-service.sh create mode 100644 services/_template/Dockerfile create mode 100644 services/_template/README.md create mode 100644 services/_template/package.json create mode 100644 services/_template/src/config/app.config.ts create mode 100644 services/_template/src/config/database.config.ts create mode 100644 services/_template/src/main.ts create mode 100644 services/_template/src/middlewares/error.middleware.ts create mode 100644 services/_template/src/middlewares/logger.middleware.ts create mode 100644 services/_template/src/modules/feature/feature.controller.ts create mode 100644 services/_template/src/modules/feature/feature.dto.ts create mode 100644 services/_template/src/modules/feature/feature.module.ts create mode 100644 services/_template/src/modules/feature/feature.service.ts create mode 100644 services/_template/src/modules/health/health.controller.ts create mode 100644 services/_template/src/routes/index.ts create mode 100644 services/_template/tsconfig.json create mode 100644 services/auth-service/Dockerfile create mode 100644 services/auth-service/README.md create mode 100644 services/auth-service/env.local.example create mode 100644 services/auth-service/package.json create mode 100644 services/auth-service/prisma/schema.prisma create mode 100644 services/auth-service/prisma/seed.d.ts create mode 100644 services/auth-service/prisma/seed.d.ts.map create mode 100644 services/auth-service/prisma/seed.js create mode 100644 services/auth-service/prisma/seed.js.map create mode 100644 services/auth-service/prisma/seed.ts create mode 100644 services/auth-service/src/config/app.config.ts create mode 100644 services/auth-service/src/config/database.config.ts create mode 100644 services/auth-service/src/config/jwt.config.ts create mode 100644 services/auth-service/src/main.ts create mode 100644 services/auth-service/src/middlewares/auth.middleware.ts create mode 100644 services/auth-service/src/middlewares/error.middleware.ts create mode 100644 services/auth-service/src/middlewares/logger.middleware.ts create mode 100644 services/auth-service/src/modules/auth/auth.controller.ts create mode 100644 services/auth-service/src/modules/auth/auth.dto.ts create mode 100644 services/auth-service/src/modules/auth/auth.module.ts create mode 100644 services/auth-service/src/modules/auth/auth.service.ts create mode 100644 services/auth-service/src/modules/health/health.controller.ts create mode 100644 services/auth-service/src/modules/user/user.controller.ts create mode 100644 services/auth-service/src/modules/user/user.dto.ts create mode 100644 services/auth-service/src/modules/user/user.module.ts create mode 100644 services/auth-service/src/modules/user/user.service.ts create mode 100644 services/auth-service/src/routes/index.ts create mode 100644 services/auth-service/tsconfig.json create mode 100644 turbo.json diff --git a/.cursor/plans/enterprise_f5e39e08.plan.md b/.cursor/plans/enterprise_f5e39e08.plan.md new file mode 100644 index 00000000..a480c144 --- /dev/null +++ b/.cursor/plans/enterprise_f5e39e08.plan.md @@ -0,0 +1,1323 @@ +--- +name: Enterprise Microservices +overview: Thiết lập cấu trúc thư mục enterprise-grade monorepo cho hệ thống microservices, bắt đầu với Auth Service, hỗ trợ Web + Mobile, và full infrastructure stack (Traefik, Observability, CI/CD). +todos: + - id: root-structure + content: Create root folder structure and workspace configuration + status: completed + - id: shared-packages + content: Setup shared packages (logger, types, config, http-client, auth-sdk, tracing) + status: completed + - id: auth-service + content: Create Auth Service with modules, database, and API endpoints + status: completed + - id: service-template + content: Create service template for future microservices + status: completed + - id: infrastructure + content: Setup Traefik, databases, and observability stack + status: completed + - id: web-app + content: Create Next.js web application with API integration + status: completed + - id: mobile-app + content: Create React Native mobile application + status: completed + - id: docker-configs + content: Create Docker and docker-compose configurations for all services + status: completed + - id: cicd-workflows + content: Setup GitHub Actions workflows for CI/CD + status: completed + - id: deployments + content: Create deployment configs for local, staging, and production + status: completed + - id: automation-scripts + content: Create helper scripts for development and deployment + status: completed + - id: documentation + content: Write comprehensive documentation and guides + status: completed +--- + +# Plan: Cấu trúc Microservices Enterprise-Grade + +## Tổng quan kiến trúc + +Dự án sẽ được tổ chức theo mô hình **monorepo** với các nguyên tắc: + +- Tách biệt rõ ràng: Apps - Services - Packages - Infra - Deployments +- Mỗi service deploy độc lập với CI/CD riêng +- Shared packages để tái sử dụng code +- Infrastructure as Code với Traefik + Docker + Observability +- Hỗ trợ cả Docker Compose (dev) và Kubernetes (prod) +```mermaid +graph TB + subgraph apps [Apps Layer] + web[Web App
NextJS] + mobile[Mobile App
React Native] + end + + subgraph gateway [API Gateway] + traefik[Traefik
Routing & Load Balancing] + end + + subgraph services [Services Layer] + auth[Auth Service
Node.js] + future[Future Services
Ready to add] + end + + subgraph packages [Shared Packages] + logger[Logger] + types[Types] + config[Config] + end + + subgraph infra [Infrastructure] + obs[Observability
Prometheus/Grafana/Loki] + db[Databases
PostgreSQL/Redis] + end + + web --> traefik + mobile --> traefik + traefik --> auth + traefik --> future + auth --> packages + auth --> db + future --> packages + obs --> auth + obs --> future +``` + + +## Cấu trúc thư mục chi tiết + +### 1. Root Level Structure + +Tạo cấu trúc thư mục gốc với các folder chính: + +``` +/Users/velikho/Desktop/WORKING/Base/ +├── apps/ # Frontend applications +├── services/ # Backend microservices +├── packages/ # Shared libraries & utilities +├── infra/ # Infrastructure as Code +├── deployments/ # Environment-specific configs +├── .github/ # CI/CD workflows +├── scripts/ # Automation scripts +├── docs/ # Documentation +├── .gitignore +├── .dockerignore +├── README.md +└── package.json # Root workspace config +``` + +### 2. Apps - Frontend Layer + +**[apps/web](apps/web)** - Next.js Web Application: + +``` +apps/web/ +├── public/ +├── src/ +│ ├── app/ # App router (Next.js 14+) +│ ├── components/ +│ │ ├── ui/ # Base UI components +│ │ ├── features/ # Feature-specific components +│ │ └── layouts/ # Layout components +│ ├── hooks/ # Custom React hooks +│ ├── services/ # API client services +│ │ ├── api/ +│ │ │ ├── auth.api.ts +│ │ │ └── client.ts +│ │ └── config.ts +│ ├── stores/ # State management (Zustand) +│ ├── styles/ # Global styles +│ ├── types/ # TypeScript types +│ └── utils/ # Helper functions +├── .env.example +├── .env.local +├── Dockerfile +├── docker-compose.dev.yml +├── next.config.js +├── package.json +├── tsconfig.json +└── README.md +``` + +**[apps/mobile](apps/mobile)** - React Native Application: + +``` +apps/mobile/ +├── src/ +│ ├── screens/ # Screen components +│ ├── components/ # Reusable components +│ ├── navigation/ # React Navigation +│ ├── services/ # API clients +│ ├── stores/ # State management +│ ├── hooks/ +│ ├── utils/ +│ └── types/ +├── ios/ +├── android/ +├── .env.example +├── Dockerfile # For CI builds +├── package.json +└── README.md +``` + +### 3. Services - Backend Microservices + +**[services/auth-service](services/auth-service)** - Authentication Service (Node.js + TypeScript): + +``` +services/auth-service/ +├── src/ +│ ├── modules/ # Feature modules +│ │ ├── auth/ +│ │ │ ├── auth.controller.ts +│ │ │ ├── auth.service.ts +│ │ │ ├── auth.dto.ts +│ │ │ └── auth.module.ts +│ │ ├── user/ +│ │ │ ├── user.controller.ts +│ │ │ ├── user.service.ts +│ │ │ ├── user.dto.ts +│ │ │ └── user.module.ts +│ │ └── health/ +│ ├── config/ # Service configuration +│ │ ├── database.config.ts +│ │ ├── jwt.config.ts +│ │ └── app.config.ts +│ ├── middlewares/ # Express middlewares +│ │ ├── auth.middleware.ts +│ │ ├── error.middleware.ts +│ │ └── logger.middleware.ts +│ ├── routes/ # API routes +│ ├── jobs/ # Background jobs +│ ├── utils/ +│ └── main.ts # Entry point +├── prisma/ # Database schema & migrations +│ ├── schema.prisma +│ ├── migrations/ +│ └── seed.ts +├── tests/ +│ ├── unit/ +│ └── integration/ +├── .env.example +├── Dockerfile +├── docker-compose.yml +├── package.json +├── tsconfig.json +└── README.md +``` + +**[services/_template](services/_template)** - Template cho services tương lai: + +- Sẵn sàng để copy và tạo services mới +- Chứa boilerplate code chuẩn + +### 4. Packages - Shared Libraries + +**[packages/logger](packages/logger)** - Centralized logging: + +```typescript +// packages/logger/src/index.ts +import winston from 'winston' +export const logger = winston.createLogger({...}) +``` + +**[packages/types](packages/types)** - Shared TypeScript types: + +```typescript +// packages/types/src/user.types.ts +export interface User { id: string; email: string; } +``` + +**[packages/config](packages/config)** - Shared configs: + +``` +packages/config/ +├── eslint-config/ # ESLint rules +├── tsconfig/ # TypeScript configs +└── prettier-config/ # Prettier rules +``` + +**[packages/http-client](packages/http-client)** - API client wrapper: + +```typescript +// packages/http-client/src/index.ts +import axios from 'axios' +export const createHttpClient = (baseURL: string) => {...} +``` + +**[packages/auth-sdk](packages/auth-sdk)** - Auth utilities: + +```typescript +// packages/auth-sdk/src/index.ts +export const verifyToken = (token: string) => {...} +``` + +**[packages/tracing](packages/tracing)** - OpenTelemetry setup: + +```typescript +// packages/tracing/src/index.ts +export const initTracing = (serviceName: string) => {...} +``` + +Cấu trúc packages: + +``` +packages/ +├── logger/ +├── types/ +├── config/ +├── http-client/ +├── auth-sdk/ +├── tracing/ +└── ui/ # Design system (optional) +``` + +### 5. Infrastructure - Traefik + Observability + +**[infra/traefik](infra/traefik)** - Traefik configuration: + +``` +infra/traefik/ +├── traefik.yml # Static configuration +├── dynamic/ # Dynamic configuration +│ ├── routes.yml # HTTP routing rules +│ ├── middlewares.yml # Rate limiting, CORS, etc +│ └── services.yml # Service definitions +├── certs/ # SSL certificates +└── README.md +``` + +Traefik routing example: + +```yaml +http: + routers: + auth-router: + rule: "Host(`api.goodgo.vn`) && PathPrefix(`/api/v1/auth`)" + service: auth-service + middlewares: + - auth-ratelimit + - cors +``` + +**[infra/docker](infra/docker)** - Docker configurations: + +``` +infra/docker/ +├── docker-compose.dev.yml # Development environment +├── docker-compose.prod.yml # Production environment +├── networks.yml # Docker networks +└── volumes.yml # Persistent volumes +``` + +**[infra/observability](infra/observability)** - Monitoring stack: + +``` +infra/observability/ +├── prometheus/ +│ ├── prometheus.yml +│ └── rules/ +├── grafana/ +│ ├── dashboards/ +│ │ ├── service-overview.json +│ │ └── auth-service.json +│ └── datasources/ +├── loki/ +│ └── loki-config.yml +└── docker-compose.observability.yml +``` + +**[infra/databases](infra/databases)** - Database configs: + +``` +infra/databases/ +├── postgres/ +│ ├── init.sql +│ └── postgres.conf +├── redis/ +│ └── redis.conf +└── docker-compose.databases.yml +``` + +**[infra/secrets](infra/secrets)** - Secrets management (not committed): + +``` +infra/secrets/ +├── .gitignore # Ignore all secrets +├── dev/ +│ └── .env.example +└── prod/ + └── .env.example +``` + +### 6. Deployments - Environment Configs + +**[deployments/local](deployments/local)** - Local development: + +``` +deployments/local/ +├── docker-compose.yml # Full stack for local dev +├── .env.local +└── README.md +``` + +**[deployments/staging](deployments/staging)** - Staging environment: + +``` +deployments/staging/ +├── kubernetes/ # K8s manifests +│ ├── auth-service.yaml +│ ├── ingress.yaml +│ └── configmap.yaml +├── helm/ # Helm charts (optional) +└── .env.staging +``` + +**[deployments/production](deployments/production)** - Production environment: + +``` +deployments/production/ +├── kubernetes/ +├── helm/ +└── .env.production +``` + +### 7. CI/CD - GitHub Actions + +**[.github/workflows](.github/workflows)** - CI/CD pipelines: + +``` +.github/ +├── workflows/ +│ ├── ci-web.yml # Web app CI +│ ├── ci-mobile.yml # Mobile app CI +│ ├── ci-auth-service.yml # Auth service CI +│ ├── docker-build.yml # Docker build & push +│ ├── deploy-staging.yml # Staging deployment +│ ├── deploy-production.yml # Production deployment +│ └── pr-checks.yml # PR validation +└── actions/ # Reusable actions + ├── setup-node/ + └── docker-build/ +``` + +CI/CD flow: + +```mermaid +graph LR + PR[Pull Request] --> Lint[Lint & Format] + Lint --> Test[Unit Tests] + Test --> Build[Build] + Build --> Docker[Docker Build] + Docker --> Push[Push to Registry] + Push --> Deploy[Deploy to Staging] + Deploy --> E2E[E2E Tests] +``` + +Example workflow structure: + +```yaml +# .github/workflows/ci-auth-service.yml +name: Auth Service CI +on: + push: + paths: + - 'services/auth-service/**' + pull_request: + paths: + - 'services/auth-service/**' +``` + +### 8. Scripts - Automation + +**[scripts](scripts)** - Helper scripts: + +``` +scripts/ +├── setup/ +│ ├── init-project.sh # Initial project setup +│ └── install-deps.sh # Install all dependencies +├── dev/ +│ ├── start-all.sh # Start all services +│ ├── start-service.sh # Start specific service +│ └── logs.sh # View service logs +├── db/ +│ ├── migrate.sh # Run migrations +│ ├── seed.sh # Seed databases +│ └── backup.sh # Backup databases +├── build/ +│ ├── build-all.sh # Build all services +│ └── build-service.sh # Build specific service +├── deploy/ +│ ├── deploy-staging.sh +│ └── deploy-prod.sh +└── utils/ + ├── create-service.sh # Generate new service from template + └── cleanup.sh # Cleanup temp files +``` + +### 9. Documentation + +**[docs](docs)** - System documentation: + +``` +docs/ +├── architecture/ +│ ├── system-design.md +│ ├── service-communication.md +│ ├── data-flow.md +│ └── diagrams/ +│ ├── system-architecture.png +│ └── auth-flow.png +├── api/ +│ ├── openapi/ +│ │ └── auth-service.yaml +│ └── postman/ +├── guides/ +│ ├── getting-started.md +│ ├── development.md +│ ├── deployment.md +│ └── troubleshooting.md +├── runbooks/ +│ ├── incident-response.md +│ └── rollback-procedure.md +└── onboarding/ + └── new-developer-guide.md +``` + +### 10. Root Configuration Files + +**Root level configs**: + +- `.gitignore` - Git ignore rules +- `.dockerignore` - Docker ignore rules +- `package.json` - Workspace configuration (npm/pnpm/yarn workspaces) +- `pnpm-workspace.yaml` - PNPM workspace config +- `turbo.json` - Turborepo configuration (optional) +- `README.md` - Project overview +- `CONTRIBUTING.md` - Contribution guidelines +- `LICENSE` - License file + +## Nguyên tắc thiết kế chính + +### Separation of Concerns + +- **Apps**: Presentation layer (UI/UX) +- **Services**: Business logic (độc lập, scalable) +- **Packages**: Shared utilities (DRY principle) +- **Infra**: Infrastructure setup (IaC) +- **Deployments**: Environment configs (12-factor app) + +### Service Independence + +- Mỗi service có database riêng (Database per service pattern) +- Mỗi service có Dockerfile riêng +- Mỗi service có CI/CD pipeline riêng +- Services giao tiếp qua HTTP/gRPC hoặc message queue + +### Shared Packages Strategy + +```typescript +// Import từ shared packages +import { logger } from '@goodgo/logger' +import { User } from '@goodgo/types' +import { createHttpClient } from '@goodgo/http-client' +``` + +### Environment Management + +- Không commit secrets vào Git +- Sử dụng `.env.example` làm template +- Production secrets qua CI/CD secrets hoặc vault + +### CI/CD Strategy + +- Path-based triggers (chỉ build service thay đổi) +- Parallel builds cho các services độc lập +- Automated testing trước khi merge +- Automated deployment sau khi merge + +## Implementation Steps (High-level) + +Khi bắt đầu thực hiện, các bước chính sẽ là: + +1. **Setup Root Structure** - Tạo folders gốc và config files +2. **Setup Packages** - Tạo shared libraries trước +3. **Setup Auth Service** - Backend service đầu tiên +4. **Setup Infrastructure** - Traefik + Databases + Observability +5. **Setup Frontend** - Web app với Next.js +6. **Setup Mobile** - React Native app +7. **Setup CI/CD** - GitHub Actions workflows +8. **Setup Deployments** - Docker Compose + K8s configs +9. **Documentation** - README và guides +10. **Testing** - Verify toàn bộ hệ thống + +## Tech Stack Summary + +**Frontend:** + +- Web: Next.js 14+ (App Router), TypeScript, Tailwind CSS +- Mobile: React Native, TypeScript +- State: Zustand +- HTTP: Axios (wrapped trong shared package) + +**Backend:** + +- Auth Service: Node.js + TypeScript + Express/NestJS +- Database: PostgreSQL (Prisma ORM) +- Cache: Redis +- Future services: Mixed (Node.js + Python support) + +**Infrastructure:** + +- Reverse Proxy: Traefik +- Containers: Docker + Docker Compose +- Orchestration: Kubernetes (production) +- Monitoring: Prometheus + Grafana + Loki +- Tracing: OpenTelemetry + +**DevOps:** + +- CI/CD: GitHub Actions +- Registry: Docker Hub / GitHub Container Registry +- Secrets: GitHub Secrets / HashiCorp Vault + +## Expected File Structure Size + +Sau khi hoàn thành, cấu trúc sẽ có: + +- ~150-200 files +- ~50-70 configuration files +- ~20-30 Docker/compose files +- ~10-15 GitHub Actions workflows +- Full documentation suite + +## Monorepo Tooling Strategy + +### PNPM Workspaces (Khuyến nghị) + +**pnpm-workspace.yaml**: + +```yaml +packages: + - 'apps/*' + - 'services/*' + - 'packages/*' +``` + +**Root package.json**: + +```json +{ + "name": "@goodgo/monorepo", + "private": true, + "workspaces": [ + "apps/*", + "services/*", + "packages/*" + ], + "scripts": { + "dev": "pnpm --parallel -r dev", + "build": "pnpm -r build", + "test": "pnpm -r test", + "lint": "pnpm -r lint" + }, + "devDependencies": { + "typescript": "^5.3.0", + "prettier": "^3.1.0", + "eslint": "^8.56.0" + } +} +``` + +### Turborepo (Optional - Để tăng tốc build) + +**turbo.json**: + +```json +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", ".next/**"] + }, + "test": { + "dependsOn": ["build"], + "outputs": [] + }, + "lint": { + "outputs": [] + }, + "dev": { + "cache": false + } + } +} +``` + +## Security Best Practices + +### 1. Authentication & Authorization + +- JWT với refresh token rotation +- Rate limiting trên Traefik +- RBAC (Role-Based Access Control) +- API key management cho service-to-service + +### 2. Network Security + +```yaml +# Traefik middleware example +http: + middlewares: + secure-headers: + headers: + sslRedirect: true + stsSeconds: 31536000 + contentTypeNosniff: true + browserXssFilter: true + frameDeny: true + + rate-limit: + rateLimit: + average: 100 + burst: 50 +``` + +### 3. Secrets Management + +- Không commit `.env` files +- Sử dụng GitHub Secrets cho CI/CD +- Kubernetes Secrets cho production +- HashiCorp Vault cho enterprise (optional) + +### 4. Database Security + +- Connection pooling +- Prepared statements (Prisma tự động) +- Encryption at rest +- Regular backups + +## Performance Optimization + +### 1. Caching Strategy + +```typescript +// Redis caching layers +- API Response Cache (TTL: 5-60s) +- Session Cache (TTL: 24h) +- Database Query Cache (TTL: varies) +``` + +### 2. Database Optimization + +- Proper indexing +- Connection pooling +- Read replicas (future) +- Query optimization với Prisma + +### 3. API Gateway Optimization + +- Request compression (gzip) +- Response caching +- Load balancing +- Circuit breaker pattern + +## Monitoring & Alerting + +### Metrics to Track + +**Service Health**: + +- Uptime +- Response time (p50, p95, p99) +- Error rate +- Request rate + +**Infrastructure**: + +- CPU usage +- Memory usage +- Disk I/O +- Network I/O + +**Business Metrics**: + +- Active users +- API usage +- Feature adoption + +### Grafana Dashboards + +1. **System Overview Dashboard** + + - All services health + - Traffic overview + - Error rates + - Response times + +2. **Auth Service Dashboard** + + - Login attempts + - Token generation rate + - Failed authentications + - Active sessions + +3. **Database Dashboard** + + - Query performance + - Connection pool usage + - Slow queries + - Database size + +### Alerting Rules + +```yaml +# Prometheus alerting example +groups: + - name: service_alerts + rules: + - alert: HighErrorRate + expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05 + for: 5m + labels: + severity: critical + annotations: + summary: "High error rate detected" + + - alert: ServiceDown + expr: up == 0 + for: 1m + labels: + severity: critical + annotations: + summary: "Service is down" +``` + +## Testing Strategy + +### 1. Unit Tests + +- Coverage target: 80%+ +- Jest cho Node.js services +- React Testing Library cho frontend + +### 2. Integration Tests + +- API endpoint testing +- Database integration tests +- Service-to-service communication + +### 3. E2E Tests + +- Playwright/Cypress cho web +- Detox cho mobile +- Critical user flows + +### 4. Load Testing + +- K6 hoặc Artillery +- Performance benchmarks +- Stress testing scenarios + +## Database Design Principles + +### Auth Service Schema Example + +```prisma +// prisma/schema.prisma +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(uuid()) + email String @unique + passwordHash String + role Role @default(USER) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + sessions Session[] + refreshTokens RefreshToken[] + + @@index([email]) + @@map("users") +} + +model Session { + id String @id @default(uuid()) + userId String + token String @unique + expiresAt DateTime + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([token]) + @@map("sessions") +} + +model RefreshToken { + id String @id @default(uuid()) + userId String + token String @unique + expiresAt DateTime + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([token]) + @@map("refresh_tokens") +} + +enum Role { + USER + ADMIN + SUPER_ADMIN +} +``` + +## API Design Standards + +### RESTful Endpoints Convention + +``` +Auth Service: +POST /api/v1/auth/register - Register new user +POST /api/v1/auth/login - Login user +POST /api/v1/auth/logout - Logout user +POST /api/v1/auth/refresh - Refresh access token +GET /api/v1/auth/me - Get current user +PUT /api/v1/auth/password - Change password +POST /api/v1/auth/forgot - Forgot password +POST /api/v1/auth/reset - Reset password + +User Management: +GET /api/v1/users - List users (admin) +GET /api/v1/users/:id - Get user by ID +PUT /api/v1/users/:id - Update user +DELETE /api/v1/users/:id - Delete user + +Health Check: +GET /health - Health check +GET /health/ready - Readiness probe +GET /health/live - Liveness probe +``` + +### Response Format Standard + +```typescript +// Success Response +{ + "success": true, + "data": { /* payload */ }, + "message": "Operation successful", + "timestamp": "2024-01-01T00:00:00.000Z" +} + +// Error Response +{ + "success": false, + "error": { + "code": "AUTH_001", + "message": "Invalid credentials", + "details": {} + }, + "timestamp": "2024-01-01T00:00:00.000Z" +} + +// Pagination Response +{ + "success": true, + "data": [ /* items */ ], + "pagination": { + "page": 1, + "limit": 20, + "total": 100, + "totalPages": 5 + }, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +## Environment Variables Template + +### Auth Service .env.example + +```bash +# Server Configuration +NODE_ENV=development +PORT=5001 +API_VERSION=v1 + +# Database +DATABASE_URL=postgresql://user:password@localhost:5432/auth_db?schema=public + +# Redis +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= + +# JWT Configuration +JWT_SECRET=your-super-secret-jwt-key-change-in-production +JWT_EXPIRES_IN=15m +JWT_REFRESH_SECRET=your-super-secret-refresh-key +JWT_REFRESH_EXPIRES_IN=7d + +# CORS +CORS_ORIGIN=http://localhost:3000,http://localhost:3001 + +# Logging +LOG_LEVEL=debug + +# External Services +EMAIL_SERVICE_URL=http://notification-service:5003 +EMAIL_FROM=noreply@goodgo.vn + +# Monitoring +PROMETHEUS_PORT=9090 +TRACING_ENABLED=true +JAEGER_ENDPOINT=http://jaeger:14268/api/traces +``` + +## Development Workflow + +### 1. Branch Strategy (Git Flow) + +``` +main (production) + └── develop (staging) + ├── feature/auth-login + ├── feature/user-profile + ├── bugfix/token-refresh + └── hotfix/security-patch +``` + +### 2. Commit Convention (Conventional Commits) + +```bash +feat: add login endpoint +fix: resolve token expiration issue +docs: update API documentation +refactor: restructure auth module +test: add unit tests for auth service +chore: update dependencies +ci: add GitHub Actions workflow +perf: optimize database queries +``` + +### 3. PR Process + +1. Create feature branch từ `develop` +2. Implement changes với tests +3. Run linter và tests locally +4. Push và create PR +5. CI/CD auto run checks +6. Code review +7. Merge vào `develop` (auto deploy staging) +8. QA testing trên staging +9. Merge vào `main` (manual deploy production) + +### 4. Local Development Commands + +```bash +# Install dependencies +pnpm install + +# Start all services +pnpm dev + +# Start specific service +pnpm --filter @goodgo/auth-service dev + +# Run tests +pnpm test + +# Run linter +pnpm lint + +# Build all +pnpm build + +# Database migrations +cd services/auth-service +pnpm prisma migrate dev + +# Seed database +pnpm prisma db seed +``` + +## Docker Best Practices + +### Multi-stage Build Example + +```dockerfile +# services/auth-service/Dockerfile +FROM node:20-alpine AS base +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Dependencies stage +FROM base AS deps +COPY package.json pnpm-lock.yaml ./ +RUN corepack enable pnpm && pnpm install --frozen-lockfile + +# Builder stage +FROM base AS builder +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN pnpm prisma generate +RUN pnpm build + +# Production stage +FROM base AS runner +ENV NODE_ENV=production +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 microservice +USER microservice + +COPY --from=builder --chown=microservice:nodejs /app/dist ./dist +COPY --from=builder --chown=microservice:nodejs /app/node_modules ./node_modules +COPY --from=builder --chown=microservice:nodejs /app/package.json ./ + +EXPOSE 5001 +CMD ["node", "dist/main.js"] +``` + +### Docker Compose for Local Development + +```yaml +# deployments/local/docker-compose.yml +version: '3.8' + +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: devuser + POSTGRES_PASSWORD: devpass + POSTGRES_DB: auth_db + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U devuser"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + + auth-service: + build: + context: ../../services/auth-service + dockerfile: Dockerfile + environment: + - DATABASE_URL=postgresql://devuser:devpass@postgres:5432/auth_db + - REDIS_HOST=redis + - NODE_ENV=development + ports: + - "5001:5001" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - ../../services/auth-service:/app + - /app/node_modules + + traefik: + image: traefik:v2.10 + command: + - "--api.insecure=true" + - "--providers.docker=true" + - "--entrypoints.web.address=:80" + ports: + - "80:80" + - "8080:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ../../infra/traefik:/etc/traefik + +volumes: + postgres_data: + redis_data: +``` + +## Migration & Rollback Strategy + +### Database Migration + +```bash +# Create migration +pnpm prisma migrate dev --name add_user_table + +# Apply migrations (production) +pnpm prisma migrate deploy + +# Rollback (manual) +# Prisma không support auto rollback +# Cần tạo migration mới để reverse changes +``` + +### Service Rollback + +```bash +# Kubernetes rollback +kubectl rollout undo deployment/auth-service + +# Docker Compose rollback +docker-compose up -d --build auth-service:previous-tag + +# GitHub Actions manual rollback +# Re-run previous successful deployment workflow +``` + +## Scalability Considerations + +### Horizontal Scaling + +```yaml +# Kubernetes HPA example +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: auth-service-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: auth-service + minReplicas: 2 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 +``` + +### Database Scaling + +- Read replicas cho read-heavy workloads +- Connection pooling (PgBouncer) +- Caching layer (Redis) +- Sharding (future consideration) + +### Caching Strategy + +```typescript +// Redis caching wrapper +import { Redis } from 'ioredis'; + +export class CacheService { + private redis: Redis; + + async get(key: string): Promise { + const data = await this.redis.get(key); + return data ? JSON.parse(data) : null; + } + + async set(key: string, value: any, ttl: number = 3600): Promise { + await this.redis.setex(key, ttl, JSON.stringify(value)); + } + + async del(key: string): Promise { + await this.redis.del(key); + } +} +``` + +## Cost Optimization + +### Development Environment + +- Use Docker Compose (free) +- Single VPS cho staging (~$10-20/month) +- Free tier databases (Neon, Supabase) + +### Production Environment + +- Kubernetes cluster (GKE/EKS) ~$50-100/month +- Managed databases ~$30-50/month +- CDN (Cloudflare) - Free tier +- Monitoring (Grafana Cloud) - Free tier +- Total estimate: $100-200/month initial + +## Next Steps + +Sau khi approve plan này, implementation sẽ bắt đầu với: + +1. **Phase 1: Foundation (Week 1)** + + - Tạo root structure và workspace config + - Setup shared packages với PNPM workspaces + - Configure ESLint, Prettier, TypeScript + +2. **Phase 2: Core Service (Week 2-3)** + + - Bootstrap Auth Service với full features + - Database setup với Prisma + - Unit & integration tests + +3. **Phase 3: Infrastructure (Week 3-4)** + + - Setup Traefik với routing rules + - Configure Prometheus, Grafana, Loki + - Docker Compose cho local dev + +4. **Phase 4: Frontend (Week 4-5)** + + - Create Next.js web app + - Create React Native mobile app + - API integration với auth service + +5. **Phase 5: CI/CD (Week 5-6)** + + - GitHub Actions workflows + - Docker build & push automation + - Deployment automation + +6. **Phase 6: Documentation (Week 6)** + + - API documentation (OpenAPI) + - Development guides + - Deployment runbooks + +**Total Timeline**: 6-8 weeks cho MVP đầy đủ chức năng \ No newline at end of file diff --git a/.cursor/skills/comment-code/SKILL.md b/.cursor/skills/comment-code/SKILL.md new file mode 100644 index 00000000..fa686ace --- /dev/null +++ b/.cursor/skills/comment-code/SKILL.md @@ -0,0 +1,572 @@ +--- +name: comment-code +description: Add bilingual code comments in Vietnamese and English for better documentation. Use when adding comments to code, documenting functions/classes, or when user requests Vietnamese/English documentation. +--- + +# Bilingual Code Comments + +Add comprehensive code comments in both Vietnamese and English to improve code readability for international and Vietnamese teams. + +## When to Use + +- Adding comments to new code +- Documenting existing code +- Creating JSDoc/TSDoc documentation +- Writing function/class descriptions +- Explaining complex logic +- Adding inline comments + +## Comment Format + +### Single-line Comments + +```typescript +// EN: Initialize database connection +// VI: Khởi tạo kết nối database +const db = await createConnection(); +``` + +### Multi-line Comments + +```typescript +/** + * EN: Validates user credentials and returns JWT token + * VI: Xác thực thông tin đăng nhập và trả về JWT token + * + * @param email - User email address / Địa chỉ email người dùng + * @param password - User password / Mật khẩu người dùng + * @returns JWT token / Mã JWT token + * @throws AuthenticationError if credentials invalid / Lỗi xác thực nếu thông tin không hợp lệ + */ +async function login(email: string, password: string): Promise { + // Implementation +} +``` + +## Language-Specific Patterns + +### TypeScript/JavaScript + +#### Function Documentation +```typescript +/** + * EN: Calculates the total price including tax and discount + * VI: Tính tổng giá bao gồm thuế và giảm giá + * + * @param basePrice - Original price / Giá gốc + * @param taxRate - Tax rate (0-1) / Tỷ lệ thuế (0-1) + * @param discount - Discount amount / Số tiền giảm giá + * @returns Final price / Giá cuối cùng + */ +function calculateTotal( + basePrice: number, + taxRate: number, + discount: number +): number { + // EN: Apply discount first + // VI: Áp dụng giảm giá trước + const discountedPrice = basePrice - discount; + + // EN: Then calculate tax + // VI: Sau đó tính thuế + const tax = discountedPrice * taxRate; + + return discountedPrice + tax; +} +``` + +#### Class Documentation +```typescript +/** + * EN: Handles user authentication and authorization + * VI: Xử lý xác thực và phân quyền người dùng + */ +export class AuthService { + /** + * EN: JWT secret key from environment + * VI: Khóa bí mật JWT từ biến môi trường + */ + private readonly jwtSecret: string; + + /** + * EN: Initialize auth service with configuration + * VI: Khởi tạo service xác thực với cấu hình + * + * @param config - Authentication configuration / Cấu hình xác thực + */ + constructor(config: AuthConfig) { + this.jwtSecret = config.jwtSecret; + } + + /** + * EN: Verify JWT token and return user payload + * VI: Xác minh JWT token và trả về thông tin người dùng + * + * @param token - JWT token to verify / JWT token cần xác minh + * @returns User payload / Thông tin người dùng + * @throws TokenExpiredError if token expired / Lỗi token hết hạn + */ + async verifyToken(token: string): Promise { + // Implementation + } +} +``` + +#### Interface/Type Documentation +```typescript +/** + * EN: User data transfer object + * VI: Đối tượng truyền dữ liệu người dùng + */ +interface UserDto { + /** EN: Unique user identifier / VI: Mã định danh duy nhất */ + id: string; + + /** EN: User email address / VI: Địa chỉ email người dùng */ + email: string; + + /** EN: User display name / VI: Tên hiển thị người dùng */ + name: string; + + /** EN: User role for authorization / VI: Vai trò người dùng để phân quyền */ + role: 'admin' | 'user' | 'guest'; +} +``` + +#### Complex Logic Comments +```typescript +async function processPayment(order: Order): Promise { + // EN: Step 1: Validate order data + // VI: Bước 1: Xác thực dữ liệu đơn hàng + if (!order.items.length) { + throw new Error('Order must have items / Đơn hàng phải có sản phẩm'); + } + + // EN: Step 2: Calculate total amount + // VI: Bước 2: Tính tổng số tiền + const total = order.items.reduce((sum, item) => { + return sum + (item.price * item.quantity); + }, 0); + + // EN: Step 3: Process payment through gateway + // VI: Bước 3: Xử lý thanh toán qua cổng thanh toán + try { + const result = await paymentGateway.charge({ + amount: total, + currency: 'VND', + orderId: order.id, + }); + + // EN: Step 4: Update order status on success + // VI: Bước 4: Cập nhật trạng thái đơn hàng khi thành công + await updateOrderStatus(order.id, 'paid'); + + return result; + } catch (error) { + // EN: Log error and mark order as failed + // VI: Ghi log lỗi và đánh dấu đơn hàng thất bại + logger.error('Payment failed', { orderId: order.id, error }); + await updateOrderStatus(order.id, 'failed'); + throw error; + } +} +``` + +### React/Next.js Components + +```typescript +/** + * EN: User profile card component + * VI: Component thẻ hồ sơ người dùng + * + * @param user - User data to display / Dữ liệu người dùng để hiển thị + * @param onEdit - Callback when edit button clicked / Callback khi nhấn nút chỉnh sửa + */ +export function UserCard({ user, onEdit }: UserCardProps) { + // EN: Local state for loading status + // VI: State cục bộ cho trạng thái loading + const [isLoading, setIsLoading] = useState(false); + + /** + * EN: Handle user profile update + * VI: Xử lý cập nhật hồ sơ người dùng + */ + const handleUpdate = async () => { + setIsLoading(true); + try { + await updateUser(user.id, user); + onEdit?.(); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {/* EN: Display user avatar / VI: Hiển thị avatar người dùng */} + {user.name} + + {/* EN: User information section / VI: Phần thông tin người dùng */} +
+

{user.name}

+

{user.email}

+
+
+ ); +} +``` + +### Prisma Schema + +```prisma +/// EN: User model for authentication and profile +/// VI: Model người dùng cho xác thực và hồ sơ +model User { + /// EN: Unique identifier + /// VI: Mã định danh duy nhất + id String @id @default(cuid()) + + /// EN: User email (unique) + /// VI: Email người dùng (duy nhất) + email String @unique + + /// EN: Hashed password + /// VI: Mật khẩu đã mã hóa + password String + + /// EN: Display name + /// VI: Tên hiển thị + name String + + /// EN: Account creation timestamp + /// VI: Thời gian tạo tài khoản + createdAt DateTime @default(now()) + + /// EN: Last update timestamp + /// VI: Thời gian cập nhật cuối + updatedAt DateTime @updatedAt + + @@map("users") +} +``` + +### Configuration Files + +```typescript +// config/database.config.ts + +/** + * EN: Database configuration for Prisma and Neon PostgreSQL + * VI: Cấu hình database cho Prisma và Neon PostgreSQL + */ +export const databaseConfig = { + /** + * EN: Database connection URL from environment + * VI: URL kết nối database từ biến môi trường + */ + url: process.env.DATABASE_URL, + + /** + * EN: Connection pool settings + * VI: Cài đặt connection pool + */ + pool: { + // EN: Minimum connections in pool + // VI: Số kết nối tối thiểu trong pool + min: 2, + + // EN: Maximum connections in pool + // VI: Số kết nối tối đa trong pool + max: 10, + }, + + /** + * EN: Enable query logging in development + * VI: Bật ghi log truy vấn trong môi trường phát triển + */ + logging: process.env.NODE_ENV === 'development', +}; +``` + +### API Routes/Controllers + +```typescript +/** + * EN: User management controller + * VI: Controller quản lý người dùng + */ +export class UserController { + /** + * EN: Get user by ID + * VI: Lấy thông tin người dùng theo ID + * + * GET /api/users/:id + */ + async getById(req: Request, res: Response) { + try { + // EN: Extract user ID from params + // VI: Lấy ID người dùng từ params + const { id } = req.params; + + // EN: Fetch user from database + // VI: Lấy người dùng từ database + const user = await this.userService.findById(id); + + // EN: Return 404 if user not found + // VI: Trả về 404 nếu không tìm thấy người dùng + if (!user) { + return res.status(404).json({ + success: false, + error: { + code: 'USER_NOT_FOUND', + message: 'User not found / Không tìm thấy người dùng', + }, + }); + } + + // EN: Return user data + // VI: Trả về dữ liệu người dùng + return res.json({ + success: true, + data: user, + }); + } catch (error) { + // EN: Handle unexpected errors + // VI: Xử lý lỗi không mong đợi + logger.error('Failed to get user', { error, userId: req.params.id }); + return res.status(500).json({ + success: false, + error: { + code: 'INTERNAL_ERROR', + message: 'Internal server error / Lỗi máy chủ nội bộ', + }, + }); + } + } +} +``` + +### Middleware + +```typescript +/** + * EN: Authentication middleware to verify JWT tokens + * VI: Middleware xác thực để kiểm tra JWT token + */ +export function authMiddleware( + req: Request, + res: Response, + next: NextFunction +) { + // EN: Extract token from Authorization header + // VI: Lấy token từ header Authorization + const authHeader = req.headers.authorization; + const token = authHeader?.replace('Bearer ', ''); + + // EN: Return 401 if no token provided + // VI: Trả về 401 nếu không có token + if (!token) { + return res.status(401).json({ + success: false, + error: { + code: 'NO_TOKEN', + message: 'Authentication required / Yêu cầu xác thực', + }, + }); + } + + try { + // EN: Verify token and extract payload + // VI: Xác minh token và lấy payload + const payload = jwt.verify(token, JWT_SECRET); + + // EN: Attach user info to request + // VI: Gắn thông tin người dùng vào request + req.user = payload; + + next(); + } catch (error) { + // EN: Return 401 if token invalid or expired + // VI: Trả về 401 nếu token không hợp lệ hoặc hết hạn + return res.status(401).json({ + success: false, + error: { + code: 'INVALID_TOKEN', + message: 'Invalid or expired token / Token không hợp lệ hoặc hết hạn', + }, + }); + } +} +``` + +## Best Practices + +### 1. Comment Placement +- Place bilingual comments together (EN first, then VI) +- Keep comments close to the code they describe +- Use JSDoc format for functions and classes + +### 2. Comment Content +- **DO**: Explain WHY, not WHAT (code shows what) +- **DO**: Document complex logic and business rules +- **DO**: Include parameter descriptions and return types +- **DO**: Document error conditions and exceptions +- **DON'T**: State the obvious +- **DON'T**: Write redundant comments + +### 3. Language Guidelines +- **English**: Use clear, concise technical English +- **Vietnamese**: Use proper Vietnamese technical terms +- Keep translations accurate and natural +- Use consistent terminology across codebase + +### 4. Special Cases + +#### TODO Comments +```typescript +// TODO EN: Implement caching for better performance +// TODO VI: Triển khai caching để cải thiện hiệu suất +``` + +#### FIXME Comments +```typescript +// FIXME EN: This causes memory leak, needs refactoring +// FIXME VI: Đoạn này gây rò rỉ bộ nhớ, cần refactor +``` + +#### WARNING Comments +```typescript +// WARNING EN: Do not modify this without updating the database schema +// WARNING VI: Không sửa đổi phần này mà không cập nhật schema database +``` + +### 5. Documentation Priority + +**High Priority** (Always document): +- Public APIs and exported functions +- Complex algorithms and business logic +- Security-critical code +- Configuration and environment setup +- Error handling strategies + +**Medium Priority** (Document when helpful): +- Helper functions with non-obvious behavior +- Data transformations +- Integration points with external services + +**Low Priority** (Optional): +- Simple getters/setters +- Self-explanatory code +- Standard CRUD operations + +## Examples by Use Case + +### Authentication Flow +```typescript +/** + * EN: Complete authentication flow with refresh token + * VI: Luồng xác thực hoàn chỉnh với refresh token + */ +export class AuthFlow { + /** + * EN: Login user and generate token pair + * VI: Đăng nhập người dùng và tạo cặp token + */ + async login(credentials: LoginDto) { + // EN: Step 1: Validate credentials + // VI: Bước 1: Xác thực thông tin đăng nhập + const user = await this.validateCredentials(credentials); + + // EN: Step 2: Generate access token (15min expiry) + // VI: Bước 2: Tạo access token (hết hạn sau 15 phút) + const accessToken = this.generateAccessToken(user); + + // EN: Step 3: Generate refresh token (7 days expiry) + // VI: Bước 3: Tạo refresh token (hết hạn sau 7 ngày) + const refreshToken = this.generateRefreshToken(user); + + // EN: Step 4: Store refresh token in database + // VI: Bước 4: Lưu refresh token vào database + await this.storeRefreshToken(user.id, refreshToken); + + return { accessToken, refreshToken }; + } +} +``` + +### Database Transaction +```typescript +/** + * EN: Transfer money between accounts with transaction + * VI: Chuyển tiền giữa các tài khoản với transaction + */ +async function transferMoney( + fromAccountId: string, + toAccountId: string, + amount: number +) { + // EN: Use transaction to ensure atomicity + // VI: Sử dụng transaction để đảm bảo tính nguyên tử + return await prisma.$transaction(async (tx) => { + // EN: Deduct from sender account + // VI: Trừ tiền từ tài khoản người gửi + await tx.account.update({ + where: { id: fromAccountId }, + data: { balance: { decrement: amount } }, + }); + + // EN: Add to receiver account + // VI: Cộng tiền vào tài khoản người nhận + await tx.account.update({ + where: { id: toAccountId }, + data: { balance: { increment: amount } }, + }); + + // EN: Create transaction record + // VI: Tạo bản ghi giao dịch + return await tx.transaction.create({ + data: { + fromAccountId, + toAccountId, + amount, + type: 'TRANSFER', + }, + }); + }); +} +``` + +## Integration with Project Rules + +When commenting code in this project: +- Follow the code organization from `project-rules` skill +- Use consistent terminology with project documentation +- Align with the API response format standards +- Document according to the testing standards +- Include security considerations where relevant + +## Quick Reference + +### Function Comment Template +```typescript +/** + * EN: [Brief description in English] + * VI: [Mô tả ngắn gọn bằng tiếng Việt] + * + * @param paramName - EN description / VI mô tả + * @returns EN description / VI mô tả + * @throws ErrorType EN when / VI khi nào + */ +``` + +### Inline Comment Template +```typescript +// EN: [English explanation] +// VI: [Giải thích tiếng Việt] +``` + +### Complex Block Template +```typescript +// EN: Step N: [What this block does] +// VI: Bước N: [Block này làm gì] +``` diff --git a/.cursor/skills/project-rules/SKILL.md b/.cursor/skills/project-rules/SKILL.md new file mode 100644 index 00000000..4b7d4cdd --- /dev/null +++ b/.cursor/skills/project-rules/SKILL.md @@ -0,0 +1,476 @@ +--- +name: project-rules +description: GoodGo Microservices Platform coding standards, architecture patterns, and development guidelines. Use when working with this monorepo to ensure code follows project conventions for services, apps, packages, infrastructure, or when making architectural decisions. +--- + +# GoodGo Project Rules + +## Architecture Overview + +This is an enterprise-grade microservices monorepo with: +- **Apps**: Next.js (web-admin, web-client) + Flutter (app-admin, app-client) +- **Services**: Node.js/TypeScript microservices with Express +- **Packages**: Shared libraries (logger, types, http-client, auth-sdk, tracing, config) +- **Infrastructure**: Traefik, Redis, Neon PostgreSQL, Observability stack +- **Deployments**: Local (Docker Compose), Staging/Production (Kubernetes) + +## Tech Stack Requirements + +### Frontend +- **Web**: Next.js 14+ with App Router, TypeScript, Tailwind CSS, Zustand +- **Mobile**: Flutter 3.x with Provider pattern +- Use shared types from `@goodgo/types` package +- API calls via `@goodgo/http-client` + +### Backend Services +- **Runtime**: Node.js 20+, TypeScript 5+ +- **Framework**: Express with modular architecture +- **Database**: Prisma ORM with Neon PostgreSQL +- **Validation**: Zod for DTOs +- **Logging**: Use `@goodgo/logger` package +- **Tracing**: Use `@goodgo/tracing` with OpenTelemetry +- **Auth**: JWT tokens, use `@goodgo/auth-sdk` + +### Infrastructure +- **API Gateway**: Traefik with path-based routing +- **Caching**: Redis for sessions/cache +- **Monitoring**: Prometheus + Grafana + Loki +- **Containerization**: Docker with multi-stage builds + +## Code Organization Standards + +### Service Structure +``` +services/service-name/ +├── src/ +│ ├── config/ # Configuration files +│ ├── modules/ # Feature modules +│ │ └── feature/ +│ │ ├── feature.controller.ts +│ │ ├── feature.service.ts +│ │ ├── feature.dto.ts +│ │ └── feature.module.ts +│ ├── middlewares/ # Express middlewares +│ ├── routes/ # Route definitions +│ └── main.ts # Entry point +├── prisma/ +│ ├── schema.prisma +│ └── seed.ts +├── Dockerfile +├── package.json +└── tsconfig.json +``` + +### Package Structure +``` +packages/package-name/ +├── src/ +│ └── index.ts # Main export +├── package.json +├── tsconfig.json +└── README.md +``` + +### App Structure +``` +apps/web-*/ +├── src/ +│ ├── app/ # Next.js App Router +│ ├── services/api/ # API clients +│ └── stores/ # Zustand stores +├── Dockerfile +└── package.json +``` + +## Naming Conventions + +### Files & Folders +- **Services**: `kebab-case` (e.g., `auth-service`, `user-service`) +- **Packages**: `kebab-case` (e.g., `http-client`, `auth-sdk`) +- **Files**: `kebab-case.type.ts` (e.g., `user.controller.ts`, `auth.service.ts`) +- **Components**: `PascalCase.tsx` for React, `snake_case.dart` for Flutter + +### Code +- **Classes**: `PascalCase` (e.g., `UserService`, `AuthController`) +- **Functions/Methods**: `camelCase` (e.g., `getUserById`, `validateToken`) +- **Constants**: `UPPER_SNAKE_CASE` (e.g., `JWT_SECRET`, `API_VERSION`) +- **Interfaces/Types**: `PascalCase` with descriptive names (e.g., `UserResponse`, `LoginDto`) + +### Package Names +- Use `@goodgo/` scope for all packages +- Format: `@goodgo/package-name` + +## Development Workflow + +### Creating New Service +1. Copy `services/_template/` as starting point +2. Update `package.json` with correct name: `@goodgo/service-name` +3. Add Prisma schema if database needed +4. Create modules following modular pattern +5. Add health check endpoint +6. Create Dockerfile with multi-stage build +7. Add to `turbo.json` if needed +8. Update documentation + +### Creating New Package +1. Create in `packages/` directory +2. Use TypeScript with strict mode +3. Export from `src/index.ts` +4. Add to `pnpm-workspace.yaml` +5. Document usage in README.md +6. Version with semantic versioning + +### Adding Dependencies +```bash +# Service/App dependency +pnpm --filter @goodgo/service-name add package-name + +# Workspace package dependency +pnpm --filter @goodgo/service-name add @goodgo/logger + +# Dev dependency +pnpm --filter @goodgo/service-name add -D @types/package-name + +# Root dependency +pnpm add -w package-name +``` + +### Database Workflow +1. Update Prisma schema in service +2. Generate migration: `pnpm --filter @goodgo/service-name prisma migrate dev` +3. Generate client: `pnpm --filter @goodgo/service-name prisma generate` +4. Use Neon PostgreSQL (no local PostgreSQL needed) +5. Connection pooling via Prisma + +## Code Quality Standards + +### TypeScript +- Enable strict mode +- No `any` types (use `unknown` if needed) +- Define interfaces for all DTOs +- Use Zod for runtime validation +- Export types from `@goodgo/types` when shared + +### Error Handling +- Use custom error classes +- Implement global error middleware +- Log errors with context using `@goodgo/logger` +- Return consistent error responses: +```typescript +{ + success: false, + error: { + code: 'ERROR_CODE', + message: 'Human readable message', + details?: any + } +} +``` + +### API Response Format +```typescript +// Success +{ + success: true, + data: any +} + +// Error +{ + success: false, + error: { + code: string, + message: string, + details?: any + } +} +``` + +### Logging +```typescript +import { logger } from '@goodgo/logger'; + +logger.info('Message', { context: 'data' }); +logger.error('Error', { error, context: 'data' }); +logger.debug('Debug info', { data }); +``` + +### Environment Variables +- Use `.env.example` as template +- Never commit `.env` files +- Validate env vars at startup +- Use Zod for env validation +- Document all env vars in README + +## Testing Standards + +### Unit Tests +- Place tests next to source: `feature.service.test.ts` +- Use Jest as test runner +- Mock external dependencies +- Aim for >80% coverage + +### Integration Tests +- Test API endpoints end-to-end +- Use test database (Neon branch) +- Clean up test data after tests + +### Test Commands +```bash +pnpm test # All tests +pnpm --filter @goodgo/service-name test # Service tests +pnpm test:coverage # With coverage +``` + +## Docker Standards + +### Multi-stage Builds +```dockerfile +# Build stage +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json pnpm-lock.yaml ./ +RUN npm install -g pnpm && pnpm install +COPY . . +RUN pnpm build + +# Production stage +FROM node:20-alpine +WORKDIR /app +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules +CMD ["node", "dist/main.js"] +``` + +### Image Naming +- Format: `goodgo/service-name:version` +- Use semantic versioning +- Tag latest for production + +## Git Workflow + +### Branch Naming +- Feature: `feature/description` +- Bug fix: `fix/description` +- Hotfix: `hotfix/description` +- Release: `release/version` + +### Commit Messages +Follow Conventional Commits: +``` +type(scope): subject + +body (optional) + +footer (optional) +``` + +Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` + +Examples: +- `feat(auth): add refresh token endpoint` +- `fix(user): resolve email validation bug` +- `docs(readme): update installation steps` + +### Pull Requests +- Use PR template +- Link related issues +- Request review from team +- Ensure CI passes +- Squash merge to main + +## CI/CD Standards + +### GitHub Actions +- Run on PR: lint, test, build +- Deploy to staging on merge to `develop` +- Deploy to production on merge to `main` +- Use secrets for sensitive data + +### Deployment Checklist +- [ ] All tests pass +- [ ] No linter errors +- [ ] Environment variables configured +- [ ] Database migrations applied +- [ ] Documentation updated +- [ ] Monitoring configured + +## Security Guidelines + +### Authentication +- Use JWT with short expiry (15min access, 7d refresh) +- Store tokens securely (httpOnly cookies for web) +- Implement refresh token rotation +- Use `@goodgo/auth-sdk` for consistency + +### Authorization +- Implement RBAC (Role-Based Access Control) +- Check permissions at service level +- Use middleware for route protection + +### Data Protection +- Hash passwords with bcrypt (cost factor 12) +- Encrypt sensitive data at rest +- Use HTTPS in production +- Sanitize user inputs +- Validate all DTOs with Zod + +### Secrets Management +- Use environment variables +- Kubernetes secrets for production +- GitHub secrets for CI/CD +- Never hardcode secrets +- Rotate secrets regularly + +## Performance Guidelines + +### Backend +- Use Redis for caching +- Implement database connection pooling +- Add pagination for list endpoints +- Use database indexes +- Implement rate limiting + +### Frontend +- Use Next.js Image optimization +- Implement code splitting +- Lazy load components +- Use React.memo for expensive renders +- Optimize bundle size + +### Database +- Use Prisma query optimization +- Add indexes for frequent queries +- Use database transactions +- Implement soft deletes +- Regular backups (Neon handles this) + +## Monitoring & Observability + +### Metrics +- Use Prometheus for metrics +- Track: request count, duration, errors +- Set up alerts for critical metrics + +### Logging +- Structured logging with `@goodgo/logger` +- Include trace IDs for correlation +- Log levels: error, warn, info, debug +- Aggregate logs with Loki + +### Tracing +- Use OpenTelemetry via `@goodgo/tracing` +- Trace cross-service requests +- Include context in traces + +## Documentation Standards + +### Code Documentation +- JSDoc for public APIs +- Inline comments for complex logic +- README.md for each service/package +- Keep docs up to date + +### API Documentation +- OpenAPI/Swagger specs in `docs/api/openapi/` +- Document all endpoints +- Include request/response examples +- Document error codes + +### Architecture Documentation +- System design in `docs/architecture/` +- Service communication patterns +- Data flow diagrams +- Decision records (ADRs) + +## Common Patterns + +### Module Pattern +```typescript +// feature.module.ts +export class FeatureModule { + controller: FeatureController; + service: FeatureService; + + constructor() { + this.service = new FeatureService(); + this.controller = new FeatureController(this.service); + } +} +``` + +### DTO Pattern +```typescript +// feature.dto.ts +import { z } from 'zod'; + +export const CreateFeatureDto = z.object({ + name: z.string().min(1), + email: z.string().email(), +}); + +export type CreateFeatureDto = z.infer; +``` + +### Controller Pattern +```typescript +// feature.controller.ts +export class FeatureController { + constructor(private service: FeatureService) {} + + async create(req: Request, res: Response) { + try { + const dto = CreateFeatureDto.parse(req.body); + const result = await this.service.create(dto); + res.json({ success: true, data: result }); + } catch (error) { + next(error); + } + } +} +``` + +### Service Pattern +```typescript +// feature.service.ts +export class FeatureService { + constructor(private prisma: PrismaClient) {} + + async create(dto: CreateFeatureDto) { + return this.prisma.feature.create({ data: dto }); + } +} +``` + +## Troubleshooting + +### Common Issues +- **Port conflicts**: Check `deployments/local/docker-compose.yml` +- **Database connection**: Verify Neon DATABASE_URL in `.env.local` +- **Module not found**: Run `pnpm install` in root +- **Build errors**: Clear `dist/` and rebuild +- **Type errors**: Run `pnpm --filter @goodgo/service-name prisma generate` + +### Debug Commands +```bash +# Check service logs +docker-compose logs -f service-name + +# Check database connection +pnpm --filter @goodgo/service-name prisma studio + +# Rebuild containers +docker-compose up -d --build + +# Check running services +docker-compose ps +``` + +## Resources + +- [Architecture Docs](../../docs/architecture/) +- [API Specs](../../docs/api/openapi/) +- [Development Guide](../../docs/guides/development.md) +- [Deployment Guide](../../docs/guides/deployment.md) +- [Neon Database Guide](../../docs/guides/neon-database.md) +- [Contributing Guide](../../CONTRIBUTING.md) diff --git a/.github/workflows/ci-auth-service.yml b/.github/workflows/ci-auth-service.yml new file mode 100644 index 00000000..b5237983 --- /dev/null +++ b/.github/workflows/ci-auth-service.yml @@ -0,0 +1,73 @@ +name: Auth Service CI + +on: + push: + paths: + - 'services/auth-service/**' + - 'packages/**' + pull_request: + paths: + - 'services/auth-service/**' + - 'packages/**' + +jobs: + lint-and-test: + runs-on: ubuntu-latest + + # Use Neon test database if available, otherwise use PostgreSQL service + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: testuser + POSTGRES_PASSWORD: testpass + POSTGRES_DB: test_db + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@v4 + + - name: Setup PNPM + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Generate Prisma Client + run: pnpm --filter @goodgo/auth-service prisma:generate + env: + # Use Neon test DB if available, otherwise fallback to local PostgreSQL + DATABASE_URL: ${{ secrets.NEON_DATABASE_URL_TEST != '' && secrets.NEON_DATABASE_URL_TEST || 'postgresql://testuser:testpass@localhost:5432/test_db' }} + + - name: Run migrations + run: pnpm --filter @goodgo/auth-service prisma migrate deploy + env: + DATABASE_URL: ${{ secrets.NEON_DATABASE_URL_TEST != '' && secrets.NEON_DATABASE_URL_TEST || 'postgresql://testuser:testpass@localhost:5432/test_db' }} + + - name: Lint + run: pnpm --filter @goodgo/auth-service lint + + - name: Type check + run: pnpm --filter @goodgo/auth-service typecheck + + - name: Build + run: pnpm --filter @goodgo/auth-service build + + - name: Test + run: pnpm --filter @goodgo/auth-service test + env: + DATABASE_URL: ${{ secrets.NEON_DATABASE_URL_TEST != '' && secrets.NEON_DATABASE_URL_TEST || 'postgresql://testuser:testpass@localhost:5432/test_db' }} diff --git a/.github/workflows/ci-mobile.yml b/.github/workflows/ci-mobile.yml new file mode 100644 index 00000000..ce651840 --- /dev/null +++ b/.github/workflows/ci-mobile.yml @@ -0,0 +1,57 @@ +name: Mobile Apps CI + +on: + push: + paths: + - 'apps/app-*/**' + - 'packages/**' + pull_request: + paths: + - 'apps/app-*/**' + - 'packages/**' + +jobs: + lint-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PNPM + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.16.0' + channel: 'stable' + + - name: Install Flutter dependencies (Admin) + run: | + cd apps/app-admin + flutter pub get + + - name: Install Flutter dependencies (Client) + run: | + cd apps/app-client + flutter pub get + + - name: Analyze Flutter code (Admin) + run: | + cd apps/app-admin + flutter analyze + + - name: Analyze Flutter code (Client) + run: | + cd apps/app-client + flutter analyze diff --git a/.github/workflows/ci-web.yml b/.github/workflows/ci-web.yml new file mode 100644 index 00000000..9a445529 --- /dev/null +++ b/.github/workflows/ci-web.yml @@ -0,0 +1,49 @@ +name: Web Apps CI + +on: + push: + paths: + - 'apps/web-*/**' + - 'packages/**' + pull_request: + paths: + - 'apps/web-*/**' + - 'packages/**' + +jobs: + lint-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PNPM + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint Web Admin + run: pnpm --filter @goodgo/web-admin lint || echo "Skipped" + + - name: Lint Web Client + run: pnpm --filter @goodgo/web-client lint || echo "Skipped" + + - name: Type check Web Admin + run: pnpm --filter @goodgo/web-admin typecheck || echo "Skipped" + + - name: Type check Web Client + run: pnpm --filter @goodgo/web-client typecheck || echo "Skipped" + + - name: Build Web Admin + run: pnpm --filter @goodgo/web-admin build || echo "Skipped" + + - name: Build Web Client + run: pnpm --filter @goodgo/web-client build || echo "Skipped" diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 00000000..3a4e29c6 --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,58 @@ +name: Deploy to Production + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + environment: production + steps: + - uses: actions/checkout@v4 + + - name: Setup PNPM + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run database migrations + run: | + cd services/auth-service + pnpm prisma generate + pnpm prisma migrate deploy + env: + DATABASE_URL: ${{ secrets.NEON_DATABASE_URL_PRODUCTION }} + + - name: Setup kubectl + uses: azure/setup-kubectl@v3 + + - name: Configure kubectl + run: | + echo "${{ secrets.KUBECONFIG_PRODUCTION }}" | base64 -d > kubeconfig + export KUBECONFIG=./kubeconfig + + - name: Deploy Auth Service + run: | + export KUBECONFIG=./kubeconfig + kubectl apply -f deployments/production/kubernetes/auth-service.yaml + kubectl apply -f deployments/production/kubernetes/configmap.yaml + kubectl apply -f deployments/production/kubernetes/ingress.yaml + kubectl rollout status deployment/auth-service -n production + + - name: Deploy Web App + run: | + export KUBECONFIG=./kubeconfig + kubectl apply -f deployments/production/kubernetes/web-app.yaml || echo "Web app deployment not configured" + kubectl rollout status deployment/web-app -n production || echo "Web app deployment not configured" diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml new file mode 100644 index 00000000..749e9b14 --- /dev/null +++ b/.github/workflows/deploy-staging.yml @@ -0,0 +1,57 @@ +name: Deploy to Staging + +on: + push: + branches: + - develop + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PNPM + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run database migrations + run: | + cd services/auth-service + pnpm prisma generate + pnpm prisma migrate deploy + env: + DATABASE_URL: ${{ secrets.NEON_DATABASE_URL_STAGING }} + + - name: Setup kubectl + uses: azure/setup-kubectl@v3 + + - name: Configure kubectl + run: | + echo "${{ secrets.KUBECONFIG_STAGING }}" | base64 -d > kubeconfig + export KUBECONFIG=./kubeconfig + + - name: Deploy Auth Service + run: | + export KUBECONFIG=./kubeconfig + kubectl apply -f deployments/staging/kubernetes/auth-service.yaml + kubectl apply -f deployments/staging/kubernetes/configmap.yaml + kubectl apply -f deployments/staging/kubernetes/ingress.yaml + kubectl rollout status deployment/auth-service -n staging + + - name: Deploy Web App + run: | + export KUBECONFIG=./kubeconfig + kubectl apply -f deployments/staging/kubernetes/web-app.yaml || echo "Web app deployment not configured" + kubectl rollout status deployment/web-app -n staging || echo "Web app deployment not configured" diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 00000000..81db41de --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,87 @@ +name: Docker Build + +on: + push: + branches: + - main + - develop + paths: + - 'services/auth-service/**' + - 'apps/web-*/**' + workflow_dispatch: + +jobs: + build-auth-service: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Auth Service + uses: docker/build-push-action@v5 + with: + context: ./services/auth-service + push: true + tags: | + goodgo/auth-service:latest + goodgo/auth-service:${{ github.sha }} + cache-from: type=registry,ref=goodgo/auth-service:buildcache + cache-to: type=registry,ref=goodgo/auth-service:buildcache,mode=max + + build-web-admin: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Web Admin + uses: docker/build-push-action@v5 + with: + context: ./apps/web-admin + push: true + tags: | + goodgo/web-admin:latest + goodgo/web-admin:${{ github.sha }} + cache-from: type=registry,ref=goodgo/web-admin:buildcache + cache-to: type=registry,ref=goodgo/web-admin:buildcache,mode=max + + build-web-client: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Web Client + uses: docker/build-push-action@v5 + with: + context: ./apps/web-client + push: true + tags: | + goodgo/web-client:latest + goodgo/web-client:${{ github.sha }} + cache-from: type=registry,ref=goodgo/web-client:buildcache + cache-to: type=registry,ref=goodgo/web-client:buildcache,mode=max diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml new file mode 100644 index 00000000..43823713 --- /dev/null +++ b/.github/workflows/pr-checks.yml @@ -0,0 +1,34 @@ +name: PR Checks + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PNPM + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint all + run: pnpm lint + + - name: Type check all + run: pnpm typecheck + + - name: Build all + run: pnpm build diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7e8f3865 --- /dev/null +++ b/.gitignore @@ -0,0 +1,82 @@ +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Testing +coverage/ +*.lcov +.nyc_output + +# Production +dist/ +build/ +.next/ +out/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +.env*.local + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.project +.classpath +.settings/ + +# Prisma +prisma/migrations/*.sql + +# Docker +.dockerignore + +# Secrets +infra/secrets/**/* +!infra/secrets/**/.env.example +!infra/secrets/**/.gitignore + +# Temporary files +*.tmp +*.temp +.cache/ +.turbo/ + +# Database +*.db +*.sqlite +*.sqlite3 + +# Certificates +*.pem +*.key +*.crt +infra/traefik/certs/* + +# Build artifacts +*.tsbuildinfo diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..083d7f76 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,93 @@ +# Contributing Guide + +Thank you for considering contributing to GoodGo Microservices Platform! + +## Development Workflow + +### Branch Strategy + +We follow Git Flow: + +- `main` - Production-ready code +- `develop` - Integration branch for features +- `feature/*` - New features +- `bugfix/*` - Bug fixes +- `hotfix/*` - Critical production fixes + +### Commit Convention + +We use [Conventional Commits](https://www.conventionalcommits.org/): + +```bash +feat: add login endpoint +fix: resolve token expiration issue +docs: update API documentation +refactor: restructure auth module +test: add unit tests for auth service +chore: update dependencies +ci: add GitHub Actions workflow +perf: optimize database queries +``` + +### Pull Request Process + +1. Create feature branch from `develop` +2. Implement changes with tests +3. Run linter and tests locally +4. Push and create PR +5. CI/CD will auto-run checks +6. Code review required +7. Merge into `develop` (auto-deploy to staging) +8. QA testing on staging +9. Merge into `main` (manual deploy to production) + +### Code Standards + +- **TypeScript**: Strict mode enabled +- **Linting**: ESLint with shared config +- **Formatting**: Prettier +- **Testing**: Minimum 80% coverage +- **Documentation**: JSDoc for public APIs + +### Running Tests + +```bash +# All tests +pnpm test + +# Specific package +pnpm --filter @goodgo/auth-service test + +# With coverage +pnpm --filter @goodgo/auth-service test:coverage +``` + +### Code Review Checklist + +- [ ] Code follows style guidelines +- [ ] Tests added/updated +- [ ] Documentation updated +- [ ] No console.logs or debug code +- [ ] Environment variables documented +- [ ] Breaking changes documented + +## Adding a New Service + +1. Copy `services/_template` to `services/new-service` +2. Update package.json name and dependencies +3. Implement service logic +4. Add tests +5. Update documentation +6. Create CI/CD workflow + +## Adding a New Package + +1. Create package in `packages/new-package` +2. Add to workspace +3. Export from index.ts +4. Add tests +5. Document usage + +## Questions? + +Feel free to open an issue or contact the team. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..7b57db99 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 GoodGo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..501077b8 --- /dev/null +++ b/README.md @@ -0,0 +1,200 @@ +# GoodGo Microservices Platform + +Enterprise-grade microservices monorepo built with modern technologies and best practices. + +## 🏗️ Architecture + +This monorepo follows a microservices architecture pattern with: + +- **Apps**: Frontend applications (Web + Mobile) +- **Services**: Backend microservices (starting with Auth Service) +- **Packages**: Shared libraries and utilities +- **Infrastructure**: Traefik, Observability, Databases +- **Deployments**: Environment-specific configurations + +## 📁 Project Structure + +``` +├── apps/ # Frontend applications +│ ├── web-admin/ # Next.js admin web application +│ ├── web-client/ # Next.js client web application +│ ├── app-admin/ # Flutter admin mobile application +│ └── app-client/ # Flutter client mobile application +├── services/ # Backend microservices +│ ├── auth-service/ # Authentication service +│ └── _template/ # Service template +├── packages/ # Shared libraries +│ ├── logger/ # Centralized logging +│ ├── types/ # TypeScript types +│ ├── config/ # Shared configs +│ ├── http-client/ # API client wrapper +│ ├── auth-sdk/ # Auth utilities +│ └── tracing/ # OpenTelemetry setup +├── infra/ # Infrastructure as Code +│ ├── traefik/ # Traefik configuration +│ ├── docker/ # Docker configs +│ ├── observability/ # Monitoring stack +│ └── databases/ # Database configs +├── deployments/ # Environment configs +│ ├── local/ # Local development +│ ├── staging/ # Staging environment +│ └── production/ # Production environment +├── .github/ # CI/CD workflows +├── scripts/ # Automation scripts +└── docs/ # Documentation +``` + +## 🚀 Getting Started + +### Prerequisites + +- Node.js >= 20.0.0 +- pnpm >= 8.0.0 +- Docker & Docker Compose +- Neon account (https://neon.tech) - for PostgreSQL database + +### Installation + +```bash +# Install dependencies +pnpm install + +# Start all services (development) +pnpm dev + +# Start specific service +pnpm --filter @goodgo/auth-service dev + +# Build all +pnpm build + +# Run tests +pnpm test + +# Run linter +pnpm lint +``` + +### Local Development + +```bash +# Setup Neon database (first time only) +./scripts/db/setup-neon.sh +# Or manually: Create .env.local with Neon DATABASE_URL + +# Start infrastructure (Redis, Traefik - no PostgreSQL needed) +cd deployments/local +docker-compose up -d + +# Run migrations +./scripts/db/migrate.sh auth-service dev + +# Start services +pnpm dev +``` + +**Note**: This project uses Neon PostgreSQL. No local PostgreSQL setup needed! + +## 🛠️ Tech Stack + +**Frontend:** +- Web: Next.js 14+ (App Router) - Admin & Client portals +- Mobile: Flutter 3.x - Admin & Client apps +- TypeScript / Dart +- Tailwind CSS +- Zustand / Provider + +**Backend:** +- Node.js + TypeScript +- Express/NestJS +- Prisma ORM +- PostgreSQL (Neon - serverless) +- Redis + +**Infrastructure:** +- Traefik (API Gateway) +- Docker & Docker Compose +- Kubernetes (production) +- Prometheus + Grafana + Loki +- OpenTelemetry + +**DevOps:** +- GitHub Actions +- PNPM Workspaces +- Turborepo + +## 📚 Documentation + +- [Architecture Overview](docs/architecture/system-design.md) +- [API Documentation](docs/api/openapi/) +- [Development Guide](docs/guides/development.md) +- [Deployment Guide](docs/guides/deployment.md) +- [Contributing Guide](CONTRIBUTING.md) + +## 🔐 Environment Variables + +### Database (Neon) + +All environments use Neon PostgreSQL: +- **Dev**: Set `DATABASE_URL` in `deployments/local/.env.local` +- **Staging/Prod**: Set in Kubernetes secrets or GitHub Secrets + +See `infra/databases/neon/README.md` for setup instructions. + +### Other Variables + +Copy `.env.example` files in each service/app and configure accordingly. + +**Important**: Never commit `.env` files. Use `.env.example` as templates. + +## 🧪 Testing + +```bash +# Run all tests +pnpm test + +# Run tests for specific package +pnpm --filter @goodgo/auth-service test + +# Run with coverage +pnpm --filter @goodgo/auth-service test:coverage +``` + +## 📦 Building + +```bash +# Build all packages and services +pnpm build + +# Build specific service +pnpm --filter @goodgo/auth-service build +``` + +## 🚢 Deployment + +See [Deployment Guide](docs/guides/deployment.md) for detailed instructions. + +### Quick Deploy + +```bash +# Local +cd deployments/local && docker-compose up -d + +# Staging (Kubernetes) +cd deployments/staging && kubectl apply -f kubernetes/ + +# Production +cd deployments/production && kubectl apply -f kubernetes/ +``` + +## 🤝 Contributing + +Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. + +## 📄 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## 👥 Team + +Built with ❤️ by the GoodGo team diff --git a/SETUP_GUIDE.md b/SETUP_GUIDE.md new file mode 100644 index 00000000..aae3a439 --- /dev/null +++ b/SETUP_GUIDE.md @@ -0,0 +1,265 @@ +# Environment Setup Guide - Hướng Dẫn Thiết Lập Môi Trường + +## 🎯 Tổng Quan + +Dự án sử dụng **Hybrid Environment Configuration** với 2 levels: + +1. **Shared Environment** (`deployments/local/.env.local`) - Configs dùng chung +2. **Service-Specific Environment** (`services//.env.local`) - Configs riêng + +### Lợi Ích + +- ✅ **JWT secrets giống nhau** → Services có thể verify tokens của nhau +- ✅ **Mỗi service có database riêng** → Đúng microservices pattern +- ✅ **Override configs dễ dàng** → REDIS_HOST khác nhau cho Docker/Native +- ✅ **Không duplicate** → Maintain dễ hơn + +## 🚀 Quick Start + +### Option 1: Sử Dụng Script (Khuyến nghị) + +```bash +# Tự động tạo tất cả env files +./scripts/dev/setup-env.sh +``` + +### Option 2: Setup Thủ Công + +**Step 1: Shared Environment** +```bash +cp deployments/local/env.local.example deployments/local/.env.local +``` + +**Step 2: Service Environment** +```bash +cp services/auth-service/env.local.example services/auth-service/.env.local +``` + +## 📝 Cấu Hình Chi Tiết + +### 1. Shared Environment (`deployments/local/.env.local`) + +Chứa configs dùng chung cho TẤT CẢ services: + +```bash +# JWT Secrets - PHẢI GIỐNG NHAU cho tất cả services +JWT_SECRET=dev-jwt-secret-change-in-production-min-32-chars +JWT_REFRESH_SECRET=dev-refresh-secret-change-in-production-min-32-chars + +# Redis (Docker hostname) +REDIS_HOST=redis +REDIS_PORT=6379 + +# Common +NODE_ENV=development +LOG_LEVEL=debug +CORS_ORIGIN=http://localhost:3000,http://localhost:3001 +``` + +### 2. Service Environment (`services/auth-service/.env.local`) + +Chứa configs RIÊNG cho từng service: + +```bash +# Database riêng cho service này +DATABASE_URL=postgresql://user:pass@ep-xxx.neon.tech/goodgo_auth_dev?sslmode=require&pgbouncer=true + +# Service configs +PORT=5001 +SERVICE_NAME=auth-service + +# Override cho native dev (Redis trong Docker) +REDIS_HOST=localhost +``` + +## 🗄️ Database Setup + +### Tạo Databases Trong Neon + +Mỗi service cần database riêng: + +1. Truy cập: https://console.neon.tech +2. Tạo databases: + - `goodgo_auth_dev` → auth-service + - `goodgo_user_dev` → user-service + - `goodgo_product_dev` → product-service + - etc. + +3. Copy connection string vào `services//.env.local`: +```bash +DATABASE_URL=postgresql://user:pass@ep-xxx.neon.tech/goodgo_auth_dev?sslmode=require&pgbouncer=true +``` + +### Run Migrations + +```bash +# Auth service +./scripts/db/migrate.sh auth-service dev + +# Seed data (optional) +./scripts/db/seed.sh auth-service +``` + +## 🔧 Cách Hoạt Động + +### Loading Order + +Services load environment theo thứ tự: + +1. **Shared env** (`deployments/local/.env.local`) + - JWT_SECRET, JWT_REFRESH_SECRET + - REDIS_HOST=redis, REDIS_PORT + - NODE_ENV, LOG_LEVEL, CORS_ORIGIN + +2. **Service env** (`.env.local`) + - DATABASE_URL (riêng cho service) + - PORT (riêng cho service) + - REDIS_HOST=localhost (override cho native dev) + +### Package.json Configuration + +```json +{ + "scripts": { + "dev": "dotenv -e ../../deployments/local/.env.local -e .env.local -- tsx watch src/main.ts" + } +} +``` + +Sử dụng `dotenv-cli` để load multiple env files. + +## 📂 File Structure + +``` +Base/ +├── deployments/local/ +│ ├── env.local.example # Template cho shared env +│ └── .env.local # Shared env (gitignored) +│ +├── services/ +│ ├── auth-service/ +│ │ ├── env.example # Old template (deprecated) +│ │ ├── env.local.example # New template cho service env +│ │ └── .env.local # Service env (gitignored) +│ │ +│ └── user-service/ +│ ├── env.local.example +│ └── .env.local +│ +└── scripts/dev/ + └── setup-env.sh # Auto setup script +``` + +## ⚠️ Important Notes + +### 1. JWT Secrets + +**MUST BE IDENTICAL** across all services: +- Services cần verify JWT tokens của nhau +- Nếu khác nhau → authentication sẽ fail + +### 2. Database URLs + +**MUST BE DIFFERENT** for each service: +- Mỗi service có database riêng (microservices pattern) +- Ví dụ: + - auth-service → `goodgo_auth_dev` + - user-service → `goodgo_user_dev` + +### 3. Redis Host + +**Khác nhau tùy môi trường:** +- **Shared env**: `REDIS_HOST=redis` (Docker hostname) +- **Service env**: `REDIS_HOST=localhost` (override cho native dev) + +Khi chạy native, Redis trong Docker → dùng `localhost` + +### 4. Git Ignore + +Tất cả `.env.local` files đã được gitignored: +- `deployments/local/.env.local` +- `services/*/.env.local` + +**NEVER commit** actual env files! + +## 🎓 Examples + +### Example 1: Auth Service Native Dev + +```bash +# 1. Setup env +cp services/auth-service/env.local.example services/auth-service/.env.local + +# 2. Edit .env.local +DATABASE_URL=postgresql://user:pass@ep-xxx.neon.tech/goodgo_auth_dev?sslmode=require +PORT=5001 +REDIS_HOST=localhost # Override cho native dev + +# 3. Start Redis in Docker +cd deployments/local && docker-compose up -d redis + +# 4. Run service +pnpm --filter @goodgo/auth-service dev +``` + +### Example 2: Multiple Services + +```bash +# 1. Setup all envs +./scripts/dev/setup-env.sh + +# 2. Edit each service's .env.local with different DATABASE_URL + +# 3. Start infrastructure +cd deployments/local && docker-compose up -d redis traefik + +# 4. Run all services +pnpm dev +``` + +## 🔍 Troubleshooting + +### Database Connection Failed + +```bash +# Check if .env.local exists +ls -la services/auth-service/.env.local + +# Check DATABASE_URL +cat services/auth-service/.env.local | grep DATABASE_URL + +# Test connection +cd services/auth-service && pnpm prisma db pull +``` + +### JWT Secret Warning + +Nếu thấy warning "Using default JWT_SECRET": +- Check `deployments/local/.env.local` có JWT_SECRET chưa +- Check service có load đúng env files chưa + +### Redis Connection Failed + +Khi chạy native: +- Check Redis container đang chạy: `docker ps | grep redis` +- Check REDIS_HOST=localhost trong service `.env.local` + +## 📚 Tài Liệu Thêm + +- [Local Development Guide](docs/vi/guides/local-development.md) +- [Neon Database Guide](docs/vi/guides/neon-database.md) +- [Service README](services/auth-service/README.md) + +## 🎉 Ready to Go! + +Sau khi setup xong: + +```bash +# Start everything +./scripts/dev/start-all.sh + +# Or selective +pnpm --filter @goodgo/auth-service dev +``` + +Happy coding! 🚀 diff --git a/apps/app-admin/.gitignore b/apps/app-admin/.gitignore new file mode 100644 index 00000000..24476c5d --- /dev/null +++ b/apps/app-admin/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/apps/app-admin/Dockerfile b/apps/app-admin/Dockerfile new file mode 100644 index 00000000..f3a6f3db --- /dev/null +++ b/apps/app-admin/Dockerfile @@ -0,0 +1,20 @@ +# Flutter build stage +FROM ghcr.io/cirruslabs/flutter:stable AS build + +WORKDIR /app + +# Copy pubspec files +COPY pubspec.yaml pubspec.lock ./ + +# Get dependencies +RUN flutter pub get + +# Copy source code +COPY . . + +# Build APK (for CI/CD purposes) +# Note: Actual mobile builds require native tooling +RUN flutter build apk --release || echo "Build skipped - requires Android SDK" + +# For CI/CD testing only +CMD ["flutter", "test"] diff --git a/apps/app-admin/README.md b/apps/app-admin/README.md new file mode 100644 index 00000000..3a9e8f89 --- /dev/null +++ b/apps/app-admin/README.md @@ -0,0 +1,43 @@ +# App Admin (Flutter) + +Flutter mobile application for GoodGo Platform Admin Panel. + +## Features + +- Flutter 3.x +- Material Design 3 +- GoRouter for navigation +- Provider for state management +- API integration + +## Prerequisites + +- Flutter SDK >= 3.0.0 +- Dart SDK >= 3.0.0 +- Android Studio / Xcode + +## Development + +```bash +# Install dependencies +flutter pub get + +# Run on Android +flutter run + +# Run on iOS +flutter run -d ios + +# Build APK +flutter build apk + +# Build iOS +flutter build ios +``` + +## Environment Variables + +Create `.env` file: +``` +API_URL=http://localhost/api/v1 +``` diff --git a/apps/app-admin/lib/main.dart b/apps/app-admin/lib/main.dart new file mode 100644 index 00000000..c6a91c3e --- /dev/null +++ b/apps/app-admin/lib/main.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'GoodGo Admin', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + useMaterial3: true, + ), + routerConfig: _router, + ); + } +} + +final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomeScreen(); + }, + ), + GoRoute( + path: '/login', + builder: (BuildContext context, GoRouterState state) { + return const LoginScreen(); + }, + ), + ], +); + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('GoodGo Admin'), + ), + body: const Center( + child: Text('Welcome to GoodGo Admin'), + ), + ); + } +} + +class LoginScreen extends StatelessWidget { + const LoginScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Login'), + ), + body: const Center( + child: Text('Login Screen'), + ), + ); + } +} diff --git a/apps/app-admin/pubspec.yaml b/apps/app-admin/pubspec.yaml new file mode 100644 index 00000000..449a10bd --- /dev/null +++ b/apps/app-admin/pubspec.yaml @@ -0,0 +1,24 @@ +name: app_admin +description: GoodGo Platform Admin Mobile Application (Flutter) +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.6 + http: ^1.1.0 + shared_preferences: ^2.2.2 + provider: ^6.1.1 + go_router: ^13.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + +flutter: + uses-material-design: true diff --git a/apps/app-client/Dockerfile b/apps/app-client/Dockerfile new file mode 100644 index 00000000..f3a6f3db --- /dev/null +++ b/apps/app-client/Dockerfile @@ -0,0 +1,20 @@ +# Flutter build stage +FROM ghcr.io/cirruslabs/flutter:stable AS build + +WORKDIR /app + +# Copy pubspec files +COPY pubspec.yaml pubspec.lock ./ + +# Get dependencies +RUN flutter pub get + +# Copy source code +COPY . . + +# Build APK (for CI/CD purposes) +# Note: Actual mobile builds require native tooling +RUN flutter build apk --release || echo "Build skipped - requires Android SDK" + +# For CI/CD testing only +CMD ["flutter", "test"] diff --git a/apps/app-client/README.md b/apps/app-client/README.md new file mode 100644 index 00000000..f9642b63 --- /dev/null +++ b/apps/app-client/README.md @@ -0,0 +1,43 @@ +# App Client (Flutter) + +Flutter mobile application for GoodGo Platform Client Portal. + +## Features + +- Flutter 3.x +- Material Design 3 +- GoRouter for navigation +- Provider for state management +- API integration + +## Prerequisites + +- Flutter SDK >= 3.0.0 +- Dart SDK >= 3.0.0 +- Android Studio / Xcode + +## Development + +```bash +# Install dependencies +flutter pub get + +# Run on Android +flutter run + +# Run on iOS +flutter run -d ios + +# Build APK +flutter build apk + +# Build iOS +flutter build ios +``` + +## Environment Variables + +Create `.env` file: +``` +API_URL=http://localhost/api/v1 +``` diff --git a/apps/app-client/lib/main.dart b/apps/app-client/lib/main.dart new file mode 100644 index 00000000..808f72b6 --- /dev/null +++ b/apps/app-client/lib/main.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'GoodGo Client', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + useMaterial3: true, + ), + routerConfig: _router, + ); + } +} + +final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomeScreen(); + }, + ), + GoRoute( + path: '/login', + builder: (BuildContext context, GoRouterState state) { + return const LoginScreen(); + }, + ), + ], +); + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('GoodGo Client'), + ), + body: const Center( + child: Text('Welcome to GoodGo Client'), + ), + ); + } +} + +class LoginScreen extends StatelessWidget { + const LoginScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Login'), + ), + body: const Center( + child: Text('Login Screen'), + ), + ); + } +} diff --git a/apps/app-client/pubspec.yaml b/apps/app-client/pubspec.yaml new file mode 100644 index 00000000..3afe4b4e --- /dev/null +++ b/apps/app-client/pubspec.yaml @@ -0,0 +1,24 @@ +name: app_client +description: GoodGo Platform Client Mobile Application (Flutter) +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.6 + http: ^1.1.0 + shared_preferences: ^2.2.2 + provider: ^6.1.1 + go_router: ^13.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + +flutter: + uses-material-design: true diff --git a/apps/web-admin/Dockerfile b/apps/web-admin/Dockerfile new file mode 100644 index 00000000..ad52506d --- /dev/null +++ b/apps/web-admin/Dockerfile @@ -0,0 +1,50 @@ +FROM node:20-alpine AS base +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Builder stage +FROM base AS builder +RUN corepack enable pnpm +# Copy workspace configuration +COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ +# Create directory structure and copy all package.json files +RUN mkdir -p packages apps services +COPY packages/auth-sdk/package.json ./packages/auth-sdk/ +COPY packages/http-client/package.json ./packages/http-client/ +COPY packages/logger/package.json ./packages/logger/ +COPY packages/tracing/package.json ./packages/tracing/ +COPY packages/types/package.json ./packages/types/ +COPY packages/config/eslint-config/package.json ./packages/config/eslint-config/ +COPY packages/config/prettier-config/package.json ./packages/config/prettier-config/ +COPY packages/config/tsconfig/package.json ./packages/config/tsconfig/ +COPY apps/web-client/package.json ./apps/web-client/ +COPY apps/web-admin/package.json ./apps/web-admin/ +COPY services/auth-service/package.json ./services/auth-service/ +# Install all dependencies for entire monorepo +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile +# Copy all source code +COPY packages ./packages +COPY apps/web-admin ./apps/web-admin +COPY turbo.json ./ +# Build using turbo from root (handles dependency order automatically) +RUN pnpm turbo build --filter=web-admin + +# Production stage +FROM base AS runner +ENV NODE_ENV=production +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Copy the entire workspace to preserve pnpm structure +COPY --from=builder --chown=nextjs:nodejs /app /app + +WORKDIR /app/apps/web-admin + +USER nextjs + +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", ".next/standalone/apps/web-admin/server.js"] diff --git a/apps/web-admin/README.md b/apps/web-admin/README.md new file mode 100644 index 00000000..58c1ca63 --- /dev/null +++ b/apps/web-admin/README.md @@ -0,0 +1,31 @@ +# Web Admin Application + +Next.js web application for GoodGo Platform Admin Panel. + +## Features + +- Next.js 14 with App Router +- TypeScript +- Tailwind CSS +- Zustand for state management +- API integration with auth service + +## Development + +```bash +# Install dependencies +pnpm install + +# Start development server +pnpm dev + +# Build for production +pnpm build + +# Start production server +pnpm start +``` + +## Environment Variables + +- `NEXT_PUBLIC_API_URL` - API base URL (default: http://localhost/api/v1) diff --git a/apps/web-admin/next-env.d.ts b/apps/web-admin/next-env.d.ts new file mode 100644 index 00000000..40c3d680 --- /dev/null +++ b/apps/web-admin/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/apps/web-admin/next.config.js b/apps/web-admin/next.config.js new file mode 100644 index 00000000..6869a1ad --- /dev/null +++ b/apps/web-admin/next.config.js @@ -0,0 +1,10 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + output: 'standalone', + env: { + NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost/api/v1', + }, +}; + +module.exports = nextConfig; diff --git a/apps/web-admin/package.json b/apps/web-admin/package.json new file mode 100644 index 00000000..6e3d7165 --- /dev/null +++ b/apps/web-admin/package.json @@ -0,0 +1,35 @@ +{ + "name": "@goodgo/web-admin", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@goodgo/types": "workspace:*", + "@goodgo/http-client": "workspace:*", + "next": "^14.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "zustand": "^4.4.7", + "axios": "^1.6.5" + }, + "devDependencies": { + "@goodgo/eslint-config": "workspace:*", + "@goodgo/tsconfig": "workspace:*", + "@goodgo/prettier-config": "workspace:*", + "@types/node": "^20.11.0", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "typescript": "^5.3.3", + "tailwindcss": "^3.4.1", + "postcss": "^8.4.33", + "autoprefixer": "^10.4.17", + "eslint": "^8.56.0", + "eslint-config-next": "^14.1.0" + } +} diff --git a/apps/web-admin/postcss.config.js b/apps/web-admin/postcss.config.js new file mode 100644 index 00000000..12a703d9 --- /dev/null +++ b/apps/web-admin/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/web-admin/public/.gitkeep b/apps/web-admin/public/.gitkeep new file mode 100644 index 00000000..580430c1 --- /dev/null +++ b/apps/web-admin/public/.gitkeep @@ -0,0 +1 @@ +# Public assets diff --git a/apps/web-admin/src/app/globals.css b/apps/web-admin/src/app/globals.css new file mode 100644 index 00000000..fd81e885 --- /dev/null +++ b/apps/web-admin/src/app/globals.css @@ -0,0 +1,27 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +} diff --git a/apps/web-admin/src/app/layout.tsx b/apps/web-admin/src/app/layout.tsx new file mode 100644 index 00000000..1596f0e9 --- /dev/null +++ b/apps/web-admin/src/app/layout.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from 'next'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'GoodGo Platform', + description: 'Enterprise microservices platform', +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/apps/web-admin/src/app/login/page.tsx b/apps/web-admin/src/app/login/page.tsx new file mode 100644 index 00000000..2759230c --- /dev/null +++ b/apps/web-admin/src/app/login/page.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useAuthStore } from '@/stores/auth.store'; + +export default function LoginPage() { + const router = useRouter(); + const { login, isLoading } = useAuthStore(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + try { + await login(email, password); + router.push('/'); + } catch (err: any) { + setError(err.message || 'Login failed'); + } + }; + + return ( +
+
+

Login

+ {error &&
{error}
} +
+ + setEmail(e.target.value)} + className="w-full p-2 border rounded" + required + /> +
+
+ + setPassword(e.target.value)} + className="w-full p-2 border rounded" + required + /> +
+ +
+
+ ); +} diff --git a/apps/web-admin/src/app/page.tsx b/apps/web-admin/src/app/page.tsx new file mode 100644 index 00000000..d935839e --- /dev/null +++ b/apps/web-admin/src/app/page.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { useAuthStore } from '@/stores/auth.store'; +import { useEffect } from 'react'; + +export default function Home() { + const { user, isAuthenticated, isLoading, fetchUser } = useAuthStore(); + + useEffect(() => { + if (!isAuthenticated && !isLoading) { + fetchUser(); + } + }, [isAuthenticated, isLoading, fetchUser]); + + if (isLoading) { + return
Loading...
; + } + + return ( +
+

GoodGo Platform

+ {isAuthenticated && user ? ( +
+

Welcome, {user.email}!

+

Role: {user.role}

+
+ ) : ( +
+

Please log in to continue.

+
+ )} +
+ ); +} diff --git a/apps/web-admin/src/services/api/auth.api.ts b/apps/web-admin/src/services/api/auth.api.ts new file mode 100644 index 00000000..bdfc446c --- /dev/null +++ b/apps/web-admin/src/services/api/auth.api.ts @@ -0,0 +1,45 @@ +import { apiClient } from './client'; +import { LoginDto, RegisterDto, AuthResponse, ApiResponse, UserResponse } from '@goodgo/types'; + +export const authApi = { + register: async (data: RegisterDto): Promise> => { + return apiClient.post('/auth/register', data); + }, + + login: async (data: LoginDto): Promise> => { + const response = await apiClient.post('/auth/login', data); + if (response.success && response.data) { + apiClient.setAuthToken(response.data.accessToken); + if (typeof window !== 'undefined') { + localStorage.setItem('refreshToken', response.data.refreshToken); + } + } + return response; + }, + + logout: async (): Promise => { + const refreshToken = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null; + const response = await apiClient.post('/auth/logout', { refreshToken }); + apiClient.removeAuthToken(); + if (typeof window !== 'undefined') { + localStorage.removeItem('refreshToken'); + } + return response; + }, + + refreshToken: async (refreshToken: string): Promise> => { + const response = await apiClient.post('/auth/refresh', { refreshToken }); + if (response.success && response.data) { + apiClient.setAuthToken(response.data.accessToken); + } + return response; + }, + + getMe: async (): Promise> => { + return apiClient.get('/users/me'); + }, + + changePassword: async (currentPassword: string, newPassword: string): Promise => { + return apiClient.put('/auth/password', { currentPassword, newPassword }); + }, +}; diff --git a/apps/web-admin/src/services/api/client.ts b/apps/web-admin/src/services/api/client.ts new file mode 100644 index 00000000..79caf82e --- /dev/null +++ b/apps/web-admin/src/services/api/client.ts @@ -0,0 +1,8 @@ +import { createHttpClient } from '@goodgo/http-client'; + +const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost/api/v1'; + +export const apiClient = createHttpClient({ + baseURL: API_URL, + timeout: 30000, +}); diff --git a/apps/web-admin/src/stores/auth.store.ts b/apps/web-admin/src/stores/auth.store.ts new file mode 100644 index 00000000..c4b6c931 --- /dev/null +++ b/apps/web-admin/src/stores/auth.store.ts @@ -0,0 +1,103 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import { UserResponse } from '@goodgo/types'; +import { authApi } from '../services/api/auth.api'; + +interface AuthState { + user: UserResponse | null; + isAuthenticated: boolean; + isLoading: boolean; + login: (email: string, password: string) => Promise; + register: (email: string, password: string, confirmPassword: string) => Promise; + logout: () => Promise; + fetchUser: () => Promise; +} + +export const useAuthStore = create()( + persist( + (set) => ({ + user: null, + isAuthenticated: false, + isLoading: false, + + login: async (email: string, password: string) => { + set({ isLoading: true }); + try { + const response = await authApi.login({ email, password }); + if (response.success && response.data) { + set({ + user: response.data.user, + isAuthenticated: true, + isLoading: false, + }); + } else { + throw new Error(response.error?.message || 'Login failed'); + } + } catch (error) { + set({ isLoading: false }); + throw error; + } + }, + + register: async (email: string, password: string, confirmPassword: string) => { + set({ isLoading: true }); + try { + const response = await authApi.register({ email, password, confirmPassword }); + if (response.success && response.data) { + set({ + user: response.data.user, + isAuthenticated: true, + isLoading: false, + }); + } else { + throw new Error(response.error?.message || 'Registration failed'); + } + } catch (error) { + set({ isLoading: false }); + throw error; + } + }, + + logout: async () => { + try { + await authApi.logout(); + } finally { + set({ + user: null, + isAuthenticated: false, + }); + } + }, + + fetchUser: async () => { + set({ isLoading: true }); + try { + const response = await authApi.getMe(); + if (response.success && response.data) { + set({ + user: response.data, + isAuthenticated: true, + isLoading: false, + }); + } else { + set({ + user: null, + isAuthenticated: false, + isLoading: false, + }); + } + } catch (error) { + set({ + user: null, + isAuthenticated: false, + isLoading: false, + }); + } + }, + }), + { + name: 'auth-storage', + partialize: (state) => ({ user: state.user, isAuthenticated: state.isAuthenticated }), + } + ) +); diff --git a/apps/web-admin/tailwind.config.js b/apps/web-admin/tailwind.config.js new file mode 100644 index 00000000..6c963768 --- /dev/null +++ b/apps/web-admin/tailwind.config.js @@ -0,0 +1,12 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './src/pages/**/*.{js,ts,jsx,tsx,mdx}', + './src/components/**/*.{js,ts,jsx,tsx,mdx}', + './src/app/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/apps/web-admin/tsconfig.json b/apps/web-admin/tsconfig.json new file mode 100644 index 00000000..5326b8a9 --- /dev/null +++ b/apps/web-admin/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "@goodgo/tsconfig/nextjs.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + }, + "isolatedModules": true + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/apps/web-client/Dockerfile b/apps/web-client/Dockerfile new file mode 100644 index 00000000..969ded0f --- /dev/null +++ b/apps/web-client/Dockerfile @@ -0,0 +1,50 @@ +FROM node:20-alpine AS base +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Builder stage +FROM base AS builder +RUN corepack enable pnpm +# Copy workspace configuration +COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ +# Create directory structure and copy all package.json files +RUN mkdir -p packages apps services +COPY packages/auth-sdk/package.json ./packages/auth-sdk/ +COPY packages/http-client/package.json ./packages/http-client/ +COPY packages/logger/package.json ./packages/logger/ +COPY packages/tracing/package.json ./packages/tracing/ +COPY packages/types/package.json ./packages/types/ +COPY packages/config/eslint-config/package.json ./packages/config/eslint-config/ +COPY packages/config/prettier-config/package.json ./packages/config/prettier-config/ +COPY packages/config/tsconfig/package.json ./packages/config/tsconfig/ +COPY apps/web-client/package.json ./apps/web-client/ +COPY apps/web-admin/package.json ./apps/web-admin/ +COPY services/auth-service/package.json ./services/auth-service/ +# Install all dependencies for entire monorepo +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile +# Copy all source code +COPY packages ./packages +COPY apps/web-client ./apps/web-client +COPY turbo.json ./ +# Build using turbo from root (handles dependency order automatically) +RUN pnpm turbo build --filter=web-client + +# Production stage +FROM base AS runner +ENV NODE_ENV=production +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Copy the entire workspace to preserve pnpm structure +COPY --from=builder --chown=nextjs:nodejs /app /app + +WORKDIR /app/apps/web-client + +USER nextjs + +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", ".next/standalone/apps/web-client/server.js"] diff --git a/apps/web-client/README.md b/apps/web-client/README.md new file mode 100644 index 00000000..1fef691f --- /dev/null +++ b/apps/web-client/README.md @@ -0,0 +1,31 @@ +# Web Client Application + +Next.js web application for GoodGo Platform Client Portal. + +## Features + +- Next.js 14 with App Router +- TypeScript +- Tailwind CSS +- Zustand for state management +- API integration with auth service + +## Development + +```bash +# Install dependencies +pnpm install + +# Start development server +pnpm dev + +# Build for production +pnpm build + +# Start production server +pnpm start +``` + +## Environment Variables + +- `NEXT_PUBLIC_API_URL` - API base URL (default: http://localhost/api/v1) diff --git a/apps/web-client/next-env.d.ts b/apps/web-client/next-env.d.ts new file mode 100644 index 00000000..40c3d680 --- /dev/null +++ b/apps/web-client/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/apps/web-client/next.config.js b/apps/web-client/next.config.js new file mode 100644 index 00000000..6869a1ad --- /dev/null +++ b/apps/web-client/next.config.js @@ -0,0 +1,10 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + output: 'standalone', + env: { + NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost/api/v1', + }, +}; + +module.exports = nextConfig; diff --git a/apps/web-client/package.json b/apps/web-client/package.json new file mode 100644 index 00000000..4b49bb35 --- /dev/null +++ b/apps/web-client/package.json @@ -0,0 +1,35 @@ +{ + "name": "@goodgo/web-client", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@goodgo/types": "workspace:*", + "@goodgo/http-client": "workspace:*", + "next": "^14.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "zustand": "^4.4.7", + "axios": "^1.6.5" + }, + "devDependencies": { + "@goodgo/eslint-config": "workspace:*", + "@goodgo/tsconfig": "workspace:*", + "@goodgo/prettier-config": "workspace:*", + "@types/node": "^20.11.0", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "typescript": "^5.3.3", + "tailwindcss": "^3.4.1", + "postcss": "^8.4.33", + "autoprefixer": "^10.4.17", + "eslint": "^8.56.0", + "eslint-config-next": "^14.1.0" + } +} diff --git a/apps/web-client/postcss.config.js b/apps/web-client/postcss.config.js new file mode 100644 index 00000000..12a703d9 --- /dev/null +++ b/apps/web-client/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/web-client/public/.gitkeep b/apps/web-client/public/.gitkeep new file mode 100644 index 00000000..580430c1 --- /dev/null +++ b/apps/web-client/public/.gitkeep @@ -0,0 +1 @@ +# Public assets diff --git a/apps/web-client/src/app/globals.css b/apps/web-client/src/app/globals.css new file mode 100644 index 00000000..fd81e885 --- /dev/null +++ b/apps/web-client/src/app/globals.css @@ -0,0 +1,27 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +} diff --git a/apps/web-client/src/app/layout.tsx b/apps/web-client/src/app/layout.tsx new file mode 100644 index 00000000..1596f0e9 --- /dev/null +++ b/apps/web-client/src/app/layout.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from 'next'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'GoodGo Platform', + description: 'Enterprise microservices platform', +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/apps/web-client/src/app/login/page.tsx b/apps/web-client/src/app/login/page.tsx new file mode 100644 index 00000000..2759230c --- /dev/null +++ b/apps/web-client/src/app/login/page.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useAuthStore } from '@/stores/auth.store'; + +export default function LoginPage() { + const router = useRouter(); + const { login, isLoading } = useAuthStore(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + try { + await login(email, password); + router.push('/'); + } catch (err: any) { + setError(err.message || 'Login failed'); + } + }; + + return ( +
+
+

Login

+ {error &&
{error}
} +
+ + setEmail(e.target.value)} + className="w-full p-2 border rounded" + required + /> +
+
+ + setPassword(e.target.value)} + className="w-full p-2 border rounded" + required + /> +
+ +
+
+ ); +} diff --git a/apps/web-client/src/app/page.tsx b/apps/web-client/src/app/page.tsx new file mode 100644 index 00000000..d935839e --- /dev/null +++ b/apps/web-client/src/app/page.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { useAuthStore } from '@/stores/auth.store'; +import { useEffect } from 'react'; + +export default function Home() { + const { user, isAuthenticated, isLoading, fetchUser } = useAuthStore(); + + useEffect(() => { + if (!isAuthenticated && !isLoading) { + fetchUser(); + } + }, [isAuthenticated, isLoading, fetchUser]); + + if (isLoading) { + return
Loading...
; + } + + return ( +
+

GoodGo Platform

+ {isAuthenticated && user ? ( +
+

Welcome, {user.email}!

+

Role: {user.role}

+
+ ) : ( +
+

Please log in to continue.

+
+ )} +
+ ); +} diff --git a/apps/web-client/src/services/api/auth.api.ts b/apps/web-client/src/services/api/auth.api.ts new file mode 100644 index 00000000..bdfc446c --- /dev/null +++ b/apps/web-client/src/services/api/auth.api.ts @@ -0,0 +1,45 @@ +import { apiClient } from './client'; +import { LoginDto, RegisterDto, AuthResponse, ApiResponse, UserResponse } from '@goodgo/types'; + +export const authApi = { + register: async (data: RegisterDto): Promise> => { + return apiClient.post('/auth/register', data); + }, + + login: async (data: LoginDto): Promise> => { + const response = await apiClient.post('/auth/login', data); + if (response.success && response.data) { + apiClient.setAuthToken(response.data.accessToken); + if (typeof window !== 'undefined') { + localStorage.setItem('refreshToken', response.data.refreshToken); + } + } + return response; + }, + + logout: async (): Promise => { + const refreshToken = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null; + const response = await apiClient.post('/auth/logout', { refreshToken }); + apiClient.removeAuthToken(); + if (typeof window !== 'undefined') { + localStorage.removeItem('refreshToken'); + } + return response; + }, + + refreshToken: async (refreshToken: string): Promise> => { + const response = await apiClient.post('/auth/refresh', { refreshToken }); + if (response.success && response.data) { + apiClient.setAuthToken(response.data.accessToken); + } + return response; + }, + + getMe: async (): Promise> => { + return apiClient.get('/users/me'); + }, + + changePassword: async (currentPassword: string, newPassword: string): Promise => { + return apiClient.put('/auth/password', { currentPassword, newPassword }); + }, +}; diff --git a/apps/web-client/src/services/api/client.ts b/apps/web-client/src/services/api/client.ts new file mode 100644 index 00000000..79caf82e --- /dev/null +++ b/apps/web-client/src/services/api/client.ts @@ -0,0 +1,8 @@ +import { createHttpClient } from '@goodgo/http-client'; + +const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost/api/v1'; + +export const apiClient = createHttpClient({ + baseURL: API_URL, + timeout: 30000, +}); diff --git a/apps/web-client/src/stores/auth.store.ts b/apps/web-client/src/stores/auth.store.ts new file mode 100644 index 00000000..c4b6c931 --- /dev/null +++ b/apps/web-client/src/stores/auth.store.ts @@ -0,0 +1,103 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import { UserResponse } from '@goodgo/types'; +import { authApi } from '../services/api/auth.api'; + +interface AuthState { + user: UserResponse | null; + isAuthenticated: boolean; + isLoading: boolean; + login: (email: string, password: string) => Promise; + register: (email: string, password: string, confirmPassword: string) => Promise; + logout: () => Promise; + fetchUser: () => Promise; +} + +export const useAuthStore = create()( + persist( + (set) => ({ + user: null, + isAuthenticated: false, + isLoading: false, + + login: async (email: string, password: string) => { + set({ isLoading: true }); + try { + const response = await authApi.login({ email, password }); + if (response.success && response.data) { + set({ + user: response.data.user, + isAuthenticated: true, + isLoading: false, + }); + } else { + throw new Error(response.error?.message || 'Login failed'); + } + } catch (error) { + set({ isLoading: false }); + throw error; + } + }, + + register: async (email: string, password: string, confirmPassword: string) => { + set({ isLoading: true }); + try { + const response = await authApi.register({ email, password, confirmPassword }); + if (response.success && response.data) { + set({ + user: response.data.user, + isAuthenticated: true, + isLoading: false, + }); + } else { + throw new Error(response.error?.message || 'Registration failed'); + } + } catch (error) { + set({ isLoading: false }); + throw error; + } + }, + + logout: async () => { + try { + await authApi.logout(); + } finally { + set({ + user: null, + isAuthenticated: false, + }); + } + }, + + fetchUser: async () => { + set({ isLoading: true }); + try { + const response = await authApi.getMe(); + if (response.success && response.data) { + set({ + user: response.data, + isAuthenticated: true, + isLoading: false, + }); + } else { + set({ + user: null, + isAuthenticated: false, + isLoading: false, + }); + } + } catch (error) { + set({ + user: null, + isAuthenticated: false, + isLoading: false, + }); + } + }, + }), + { + name: 'auth-storage', + partialize: (state) => ({ user: state.user, isAuthenticated: state.isAuthenticated }), + } + ) +); diff --git a/apps/web-client/tailwind.config.js b/apps/web-client/tailwind.config.js new file mode 100644 index 00000000..6c963768 --- /dev/null +++ b/apps/web-client/tailwind.config.js @@ -0,0 +1,12 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './src/pages/**/*.{js,ts,jsx,tsx,mdx}', + './src/components/**/*.{js,ts,jsx,tsx,mdx}', + './src/app/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/apps/web-client/tsconfig.json b/apps/web-client/tsconfig.json new file mode 100644 index 00000000..5326b8a9 --- /dev/null +++ b/apps/web-client/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "@goodgo/tsconfig/nextjs.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + }, + "isolatedModules": true + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/deployments/local/README.md b/deployments/local/README.md new file mode 100644 index 00000000..524e6aa1 --- /dev/null +++ b/deployments/local/README.md @@ -0,0 +1,81 @@ +# Local Development Setup + +Docker Compose configuration for local development. + +## Prerequisites + +- Docker & Docker Compose installed +- Neon account (https://neon.tech) - for database +- Ports available: 80, 6379, 5001, 3000, 3001, 8080 + +## Initial Setup + +### 1. Setup Neon Database + +```bash +# Run setup script +./scripts/db/setup-neon.sh + +# Or manually: +# 1. Create Neon project at https://neon.tech +# 2. Get connection string from main branch +# 3. Create deployments/local/.env.local: +# DATABASE_URL=postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true +``` + +See [Neon Setup Guide](../../infra/databases/neon/README.md) for details. + +### 2. Start Services + +```bash +# Start infrastructure (Redis, Traefik - no PostgreSQL needed) +docker-compose up -d + +# Run migrations +./scripts/db/migrate.sh auth-service dev + +# Seed database (optional) +./scripts/db/seed.sh auth-service +``` + +## Usage + +```bash +# Start all services +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop all services +docker-compose down + +# Stop and remove volumes (clean slate) +docker-compose down -v +``` + +## Services + +- **Neon Database**: Cloud-hosted (no local container) +- **Redis**: `localhost:6379` +- **Auth Service**: `localhost:5001` +- **Web Admin**: `http://localhost:3000` or `http://admin.localhost` +- **Web Client**: `http://localhost:3001` or `http://localhost` +- **Traefik Dashboard**: `http://localhost:8080` + +## Access + +- API Gateway: `http://localhost/api/v1` +- Web Admin: `http://admin.localhost` (via Traefik) or `http://localhost:3000` (direct) +- Web Client: `http://localhost` (via Traefik) or `http://localhost:3001` (direct) +- Traefik Dashboard: `http://localhost:8080` + +## Environment Variables + +Copy `env.local.example` to `.env.local` and add your Neon DATABASE_URL: + +```bash +DATABASE_URL=postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true +``` + +**Note**: PostgreSQL is not included in Docker Compose. All environments use Neon database. diff --git a/deployments/local/docker-compose.yml b/deployments/local/docker-compose.yml new file mode 100644 index 00000000..e0653f4b --- /dev/null +++ b/deployments/local/docker-compose.yml @@ -0,0 +1,120 @@ +version: '3.8' + +# NOTE: This setup uses Neon PostgreSQL database +# Setup Neon database URL in .env.local file +# See infra/databases/neon/README.md for setup instructions + +services: + redis: + image: redis:7-alpine + container_name: redis-cache-local + command: redis-server /etc/redis/redis.conf + ports: + - "6379:6379" + volumes: + - redis_data:/data + - ../../infra/databases/redis/redis.conf:/etc/redis/redis.conf + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + networks: + - microservices-network + + auth-service: + build: + context: ../.. + dockerfile: services/auth-service/Dockerfile + container_name: auth-service-local + env_file: + - .env.local + environment: + - NODE_ENV=development + - PORT=5001 + # DATABASE_URL should be set in .env.local (Neon database URL) + - REDIS_HOST=redis + - REDIS_PORT=6379 + - JWT_SECRET=${JWT_SECRET:-dev-jwt-secret-change-in-production} + - JWT_EXPIRES_IN=15m + - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET:-dev-refresh-secret-change-in-production} + - JWT_REFRESH_EXPIRES_IN=7d + - CORS_ORIGIN=http://localhost:3000,http://localhost:3001 + - LOG_LEVEL=debug + - SERVICE_NAME=auth-service + ports: + - "5001:5001" + depends_on: + redis: + condition: service_healthy + networks: + - microservices-network + + web-admin: + build: + context: ../.. + dockerfile: apps/web-admin/Dockerfile + container_name: web-admin-local + environment: + - NODE_ENV=development + - NEXT_PUBLIC_API_URL=http://localhost/api/v1 + ports: + - "3000:3000" + depends_on: + - auth-service + networks: + - microservices-network + labels: + - "traefik.enable=true" + - "traefik.http.routers.web-admin.rule=Host(`admin.localhost`)" + - "traefik.http.routers.web-admin.entrypoints=web" + - "traefik.http.services.web-admin.loadbalancer.server.port=3000" + + web-client: + build: + context: ../.. + dockerfile: apps/web-client/Dockerfile + container_name: web-client-local + environment: + - NODE_ENV=development + - NEXT_PUBLIC_API_URL=http://localhost/api/v1 + ports: + - "3001:3000" + depends_on: + - auth-service + networks: + - microservices-network + labels: + - "traefik.enable=true" + - "traefik.http.routers.web-client.rule=Host(`localhost`)" + - "traefik.http.routers.web-client.entrypoints=web" + - "traefik.http.services.web-client.loadbalancer.server.port=3000" + + traefik: + image: traefik:v2.10 + container_name: traefik-local + command: + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + - "--log.level=INFO" + ports: + - "80:80" + - "8080:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ../../infra/traefik:/etc/traefik + networks: + - microservices-network + labels: + - "traefik.enable=true" + - "traefik.http.routers.traefik.rule=Host(`traefik.localhost`)" + - "traefik.http.routers.traefik.entrypoints=web" + +volumes: + redis_data: + +networks: + microservices-network: + driver: bridge diff --git a/deployments/local/env.local.example b/deployments/local/env.local.example new file mode 100644 index 00000000..418a62f8 --- /dev/null +++ b/deployments/local/env.local.example @@ -0,0 +1,54 @@ +# Local Development Environment Variables (Shared Configs) +# Shared Environment Variables - Shared across all services +# Copy this file to .env.local and fill in your values +# +# Note: Service-specific configs (DATABASE_URL, PORT) should be in services//.env.local + +# ============================================================================= +# SHARED SECRETS - Must be same across all services for JWT token verification +# ============================================================================= +JWT_SECRET=dev-jwt-secret-change-in-production-min-32-chars +JWT_REFRESH_SECRET=dev-refresh-secret-change-in-production-min-32-chars +JWT_EXPIRES_IN=15m +JWT_REFRESH_EXPIRES_IN=7d + +# ============================================================================= +# SHARED INFRASTRUCTURE - Redis, Traefik +# ============================================================================= +# Redis (Docker container name when using docker-compose) +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD= + +# ============================================================================= +# COMMON CONFIGURATION +# ============================================================================= +NODE_ENV=development +LOG_LEVEL=debug + +# CORS - Allowed origins for all services +CORS_ORIGIN=http://localhost:3000,http://localhost:3001,http://admin.localhost + +# ============================================================================= +# MONITORING & TRACING (Optional) +# ============================================================================= +TRACING_ENABLED=false +JAEGER_ENDPOINT=http://jaeger:14268/api/traces + +# ============================================================================= +# EXTERNAL SERVICES (Optional) +# ============================================================================= +EMAIL_FROM=noreply@goodgo.vn + +# ============================================================================= +# NOTES +# ============================================================================= +# - Each service should have its own .env.local for service-specific configs: +# * DATABASE_URL (each service has its own database) +# * PORT (each service has different port) +# * SERVICE_NAME +# * REDIS_HOST=localhost (override when running native, Redis in Docker) +# +# - Get Neon database URLs from: https://console.neon.tech +# - Create separate databases for each service (microservices pattern) +# - JWT secrets MUST be identical across all services diff --git a/deployments/production/env.production.example b/deployments/production/env.production.example new file mode 100644 index 00000000..352647a2 --- /dev/null +++ b/deployments/production/env.production.example @@ -0,0 +1,19 @@ +# Production Environment Variables +# Use these values to create Kubernetes secrets + +# Neon Database (Production branch) +DATABASE_URL=postgresql://user:password@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true + +# JWT Secrets (use strong random strings - min 32 chars) +JWT_SECRET=your-production-jwt-secret-min-32-chars +JWT_REFRESH_SECRET=your-production-refresh-secret-min-32-chars + +# Redis +REDIS_HOST=redis-service +REDIS_PORT=6379 + +# Notes: +# - Store these in Kubernetes secrets or GitHub Secrets +# - Never commit actual values to Git +# - Use kubectl create secret or GitHub Secrets for CI/CD +# - Rotate secrets regularly diff --git a/deployments/production/kubernetes/auth-service.yaml b/deployments/production/kubernetes/auth-service.yaml new file mode 100644 index 00000000..e23f4ed7 --- /dev/null +++ b/deployments/production/kubernetes/auth-service.yaml @@ -0,0 +1,91 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: auth-service + namespace: production +spec: + replicas: 3 + selector: + matchLabels: + app: auth-service + template: + metadata: + labels: + app: auth-service + spec: + containers: + - name: auth-service + image: goodgo/auth-service:latest + imagePullPolicy: Always + ports: + - containerPort: 5001 + envFrom: + - configMapRef: + name: auth-service-config + - secretRef: + name: auth-service-secrets + resources: + requests: + memory: "512Mi" + cpu: "500m" + limits: + memory: "1Gi" + cpu: "1000m" + livenessProbe: + httpGet: + path: /health/live + port: 5001 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health/ready + port: 5001 + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + +--- +apiVersion: v1 +kind: Service +metadata: + name: auth-service + namespace: production +spec: + selector: + app: auth-service + ports: + - protocol: TCP + port: 5001 + targetPort: 5001 + type: ClusterIP + +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: auth-service-hpa + namespace: production +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: auth-service + minReplicas: 3 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 diff --git a/deployments/production/kubernetes/configmap.yaml b/deployments/production/kubernetes/configmap.yaml new file mode 100644 index 00000000..b3e41697 --- /dev/null +++ b/deployments/production/kubernetes/configmap.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: auth-service-config + namespace: production +data: + NODE_ENV: "production" + PORT: "5001" + API_VERSION: "v1" + CORS_ORIGIN: "https://goodgo.vn" + LOG_LEVEL: "warn" + SERVICE_NAME: "auth-service" + TRACING_ENABLED: "true" + # Note: DATABASE_URL is stored in secrets (auth-service-secrets) + # DATABASE_URL should point to Neon production branch \ No newline at end of file diff --git a/deployments/production/kubernetes/ingress.yaml b/deployments/production/kubernetes/ingress.yaml new file mode 100644 index 00000000..4ccbfde1 --- /dev/null +++ b/deployments/production/kubernetes/ingress.yaml @@ -0,0 +1,32 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: api-ingress + namespace: production + annotations: + traefik.ingress.kubernetes.io/rule-type: PathPrefix + cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + ingressClassName: traefik + tls: + - hosts: + - api.goodgo.vn + secretName: api-tls-cert + rules: + - host: api.goodgo.vn + http: + paths: + - path: /api/v1/auth + pathType: Prefix + backend: + service: + name: auth-service + port: + number: 5001 + - path: /api/v1/users + pathType: Prefix + backend: + service: + name: auth-service + port: + number: 5001 diff --git a/deployments/production/kubernetes/secrets.yaml.example b/deployments/production/kubernetes/secrets.yaml.example new file mode 100644 index 00000000..22c503e9 --- /dev/null +++ b/deployments/production/kubernetes/secrets.yaml.example @@ -0,0 +1,34 @@ +# Kubernetes Secrets Template for Production +# DO NOT commit actual secrets to Git +# Use this as a template to create secrets + +# Create secret using kubectl: +# kubectl create secret generic auth-service-secrets \ +# --from-literal=database-url='postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true' \ +# --from-literal=jwt-secret='your-production-jwt-secret-min-32-chars' \ +# --from-literal=jwt-refresh-secret='your-production-refresh-secret-min-32-chars' \ +# --from-literal=redis-password='' \ +# -n production + +# Or use GitHub Secrets in CI/CD: +# - NEON_DATABASE_URL_PRODUCTION +# - JWT_SECRET_PRODUCTION +# - JWT_REFRESH_SECRET_PRODUCTION + +apiVersion: v1 +kind: Secret +metadata: + name: auth-service-secrets + namespace: production +type: Opaque +stringData: + # Neon Database URL (Production branch) + # Format: postgresql://user:password@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true + database-url: "postgresql://user:password@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true" + + # JWT Secrets (use strong random strings, min 32 characters) + jwt-secret: "your-production-jwt-secret-min-32-chars" + jwt-refresh-secret: "your-production-refresh-secret-min-32-chars" + + # Redis (if password protected) + redis-password: "" diff --git a/deployments/staging/env.staging.example b/deployments/staging/env.staging.example new file mode 100644 index 00000000..01025a4d --- /dev/null +++ b/deployments/staging/env.staging.example @@ -0,0 +1,18 @@ +# Staging Environment Variables +# Use these values to create Kubernetes secrets + +# Neon Database (Staging branch) +DATABASE_URL=postgresql://user:password@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true + +# JWT Secrets (use strong random strings) +JWT_SECRET=your-staging-jwt-secret-min-32-chars +JWT_REFRESH_SECRET=your-staging-refresh-secret-min-32-chars + +# Redis +REDIS_HOST=redis-service +REDIS_PORT=6379 + +# Notes: +# - Store these in Kubernetes secrets or GitHub Secrets +# - Never commit actual values to Git +# - Use kubectl create secret or GitHub Secrets for CI/CD diff --git a/deployments/staging/kubernetes/auth-service.yaml b/deployments/staging/kubernetes/auth-service.yaml new file mode 100644 index 00000000..762c9059 --- /dev/null +++ b/deployments/staging/kubernetes/auth-service.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: auth-service + namespace: staging +spec: + replicas: 2 + selector: + matchLabels: + app: auth-service + template: + metadata: + labels: + app: auth-service + spec: + containers: + - name: auth-service + image: goodgo/auth-service:latest + ports: + - containerPort: 5001 + env: + - name: NODE_ENV + value: "staging" + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: auth-service-secrets + key: database-url + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: auth-service-secrets + key: jwt-secret + - name: REDIS_HOST + value: "redis-service" + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: /health/live + port: 5001 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health/ready + port: 5001 + initialDelaySeconds: 10 + periodSeconds: 5 + +--- +apiVersion: v1 +kind: Service +metadata: + name: auth-service + namespace: staging +spec: + selector: + app: auth-service + ports: + - protocol: TCP + port: 5001 + targetPort: 5001 + type: ClusterIP diff --git a/deployments/staging/kubernetes/configmap.yaml b/deployments/staging/kubernetes/configmap.yaml new file mode 100644 index 00000000..69da0159 --- /dev/null +++ b/deployments/staging/kubernetes/configmap.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: auth-service-config + namespace: staging +data: + NODE_ENV: "staging" + PORT: "5001" + API_VERSION: "v1" + CORS_ORIGIN: "https://staging.goodgo.vn" + LOG_LEVEL: "info" + SERVICE_NAME: "auth-service" + # Note: DATABASE_URL is stored in secrets (auth-service-secrets) + # DATABASE_URL should point to Neon staging branch \ No newline at end of file diff --git a/deployments/staging/kubernetes/ingress.yaml b/deployments/staging/kubernetes/ingress.yaml new file mode 100644 index 00000000..ea329933 --- /dev/null +++ b/deployments/staging/kubernetes/ingress.yaml @@ -0,0 +1,27 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: api-ingress + namespace: staging + annotations: + traefik.ingress.kubernetes.io/rule-type: PathPrefix +spec: + ingressClassName: traefik + rules: + - host: api.staging.goodgo.vn + http: + paths: + - path: /api/v1/auth + pathType: Prefix + backend: + service: + name: auth-service + port: + number: 5001 + - path: /api/v1/users + pathType: Prefix + backend: + service: + name: auth-service + port: + number: 5001 diff --git a/deployments/staging/kubernetes/secrets.yaml.example b/deployments/staging/kubernetes/secrets.yaml.example new file mode 100644 index 00000000..0666a35e --- /dev/null +++ b/deployments/staging/kubernetes/secrets.yaml.example @@ -0,0 +1,34 @@ +# Kubernetes Secrets Template for Staging +# DO NOT commit actual secrets to Git +# Use this as a template to create secrets + +# Create secret using kubectl: +# kubectl create secret generic auth-service-secrets \ +# --from-literal=database-url='postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true' \ +# --from-literal=jwt-secret='your-staging-jwt-secret-min-32-chars' \ +# --from-literal=jwt-refresh-secret='your-staging-refresh-secret-min-32-chars' \ +# --from-literal=redis-password='' \ +# -n staging + +# Or use GitHub Secrets in CI/CD: +# - NEON_DATABASE_URL_STAGING +# - JWT_SECRET_STAGING +# - JWT_REFRESH_SECRET_STAGING + +apiVersion: v1 +kind: Secret +metadata: + name: auth-service-secrets + namespace: staging +type: Opaque +stringData: + # Neon Database URL (Staging branch) + # Format: postgresql://user:password@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true + database-url: "postgresql://user:password@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true" + + # JWT Secrets (use strong random strings, min 32 characters) + jwt-secret: "your-staging-jwt-secret-min-32-chars" + jwt-refresh-secret: "your-staging-refresh-secret-min-32-chars" + + # Redis (if password protected) + redis-password: "" diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..989cc10c --- /dev/null +++ b/docs/README.md @@ -0,0 +1,45 @@ +# Documentation + +This directory contains documentation for the GoodGo Microservices Platform, organized by language. + +## Structure + +``` +docs/ +├── en/ # English documentation +│ ├── api/ +│ ├── architecture/ +│ ├── guides/ +│ ├── onboarding/ +│ └── runbooks/ +├── vi/ # Vietnamese documentation (Tiếng Việt) +│ ├── api/ +│ ├── architecture/ +│ ├── guides/ +│ ├── onboarding/ +│ └── runbooks/ +└── README.md # This file +``` + +## Available Documentation + +### English (`/en`) +- **API**: OpenAPI specifications +- **Architecture**: System design and service communication patterns +- **Guides**: Development, deployment, getting started, troubleshooting +- **Onboarding**: New developer guide +- **Runbooks**: Incident response and rollback procedures + +### Vietnamese (`/vi`) +- **API**: OpenAPI specifications +- **Architecture**: Thiết kế hệ thống và các mẫu giao tiếp service +- **Guides**: Development, deployment, bắt đầu, xử lý sự cố +- **Onboarding**: Hướng dẫn cho developer mới +- **Runbooks**: Phản ứng sự cố và quy trình rollback + +## Contributing + +When adding new documentation: +1. Add the English version to `/en` +2. Add the Vietnamese translation to `/vi` +3. Keep both versions in sync diff --git a/docs/en/api/openapi/auth-service.yaml b/docs/en/api/openapi/auth-service.yaml new file mode 100644 index 00000000..9a3c1606 --- /dev/null +++ b/docs/en/api/openapi/auth-service.yaml @@ -0,0 +1,102 @@ +openapi: 3.0.0 +info: + title: Auth Service API + version: 1.0.0 + description: Authentication and Authorization Service API + +servers: + - url: http://localhost/api/v1 + description: Local development + - url: https://api.goodgo.vn/api/v1 + description: Production + +paths: + /auth/register: + post: + summary: Register new user + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + format: email + password: + type: string + minLength: 6 + confirmPassword: + type: string + responses: + '201': + description: User registered successfully + '400': + description: Validation error + + /auth/login: + post: + summary: Login user + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + format: email + password: + type: string + responses: + '200': + description: Login successful + '401': + description: Invalid credentials + + /auth/logout: + post: + summary: Logout user + security: + - bearerAuth: [] + responses: + '200': + description: Logout successful + + /auth/refresh: + post: + summary: Refresh access token + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + refreshToken: + type: string + responses: + '200': + description: Token refreshed + '401': + description: Invalid refresh token + + /users/me: + get: + summary: Get current user + security: + - bearerAuth: [] + responses: + '200': + description: User information + '401': + description: Unauthorized + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/docs/en/architecture/service-communication.md b/docs/en/architecture/service-communication.md new file mode 100644 index 00000000..1b615442 --- /dev/null +++ b/docs/en/architecture/service-communication.md @@ -0,0 +1,58 @@ +# Service Communication + +## Communication Patterns + +### Synchronous Communication (HTTP/REST) + +Services communicate synchronously via HTTP REST APIs through Traefik API Gateway. + +**Example:** +```typescript +// Web App -> Auth Service +const response = await fetch('http://api.goodgo.vn/api/v1/auth/login', { + method: 'POST', + body: JSON.stringify({ email, password }), +}); +``` + +### Service-to-Service Communication + +Services can communicate directly via internal network: + +```typescript +// Auth Service -> Notification Service (future) +const response = await fetch('http://notification-service:5003/api/v1/notifications', { + method: 'POST', + headers: { 'X-Service-Auth': process.env.INTERNAL_API_KEY }, + body: JSON.stringify({ userId, message }), +}); +``` + +## API Gateway Routing + +Traefik routes requests based on: +- Host header (`api.goodgo.vn`) +- Path prefix (`/api/v1/auth`) + +## Error Handling + +All services follow consistent error response format: + +```json +{ + "success": false, + "error": { + "code": "AUTH_001", + "message": "Invalid credentials", + "details": {} + }, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +## Retry and Circuit Breaker + +Future implementation: +- Exponential backoff for retries +- Circuit breaker pattern for fault tolerance +- Fallback mechanisms diff --git a/docs/en/architecture/system-design.md b/docs/en/architecture/system-design.md new file mode 100644 index 00000000..1795e3a3 --- /dev/null +++ b/docs/en/architecture/system-design.md @@ -0,0 +1,81 @@ +# System Design + +## Overview + +GoodGo Microservices Platform is built using a microservices architecture pattern with the following principles: + +- **Service Independence**: Each service has its own database and can be deployed independently +- **API Gateway**: Traefik handles routing, load balancing, and cross-cutting concerns +- **Shared Libraries**: Common functionality is extracted into shared packages +- **Infrastructure as Code**: All infrastructure configurations are versioned +- **Observability**: Full monitoring, logging, and tracing capabilities + +## Architecture Diagram + +``` +┌─────────────┐ ┌─────────────┐ +│ Web App │ │ Mobile App │ +│ (Next.js) │ │ (React Native) +└──────┬──────┘ └──────┬──────┘ + │ │ + └──────────┬────────┘ + │ + ┌────────▼────────┐ + │ Traefik │ + │ (API Gateway) │ + └────────┬─────────┘ + │ + ┌─────────────┼─────────────┐ + │ │ │ +┌───▼────┐ ┌───▼────┐ ┌───▼────┐ +│ Auth │ │ Future │ │ Future │ +│Service │ │Service │ │Service │ +└───┬────┘ └───┬────┘ └───┬────┘ + │ │ │ + └────────────┼────────────┘ + │ + ┌────────────┼────────────┐ + │ │ │ +┌───▼────┐ ┌───▼────┐ ┌───▼────┐ +│Postgres│ │ Redis │ │Prometheus│ +└────────┘ └────────┘ └─────────┘ +``` + +## Components + +### Frontend Layer +- **Web App**: Next.js application with App Router +- **Mobile App**: React Native application + +### API Gateway +- **Traefik**: Reverse proxy, load balancer, SSL termination + +### Services Layer +- **Auth Service**: Authentication and authorization +- **Future Services**: Payment, Order, Notification, etc. + +### Infrastructure Layer +- **PostgreSQL**: Primary database +- **Redis**: Caching and session storage +- **Prometheus**: Metrics collection +- **Grafana**: Metrics visualization +- **Loki**: Log aggregation + +## Communication Patterns + +- **Synchronous**: HTTP/REST for request-response patterns +- **Asynchronous**: Message queues (future implementation) +- **Service Discovery**: Docker networking and Kubernetes DNS + +## Data Management + +- **Database per Service**: Each service owns its data +- **API Composition**: Services expose APIs for data access +- **Event Sourcing**: Future consideration for audit trails + +## Security + +- **Authentication**: JWT tokens with refresh token rotation +- **Authorization**: Role-based access control (RBAC) +- **Network Security**: TLS/SSL, rate limiting, CORS +- **Secrets Management**: Environment variables, Kubernetes secrets diff --git a/docs/en/guides/deployment.md b/docs/en/guides/deployment.md new file mode 100644 index 00000000..ec7fc530 --- /dev/null +++ b/docs/en/guides/deployment.md @@ -0,0 +1,106 @@ +# Deployment Guide + +## Database Setup (Neon) + +All environments use **Neon PostgreSQL**. Setup once before deployment: + +1. Create Neon project at https://neon.tech +2. Create branches: `main` (dev), `staging`, `production` +3. Get connection strings for each branch +4. Configure in environment variables (see below) + +See [Neon Setup Guide](../../infra/databases/neon/README.md) for details. + +## Local Deployment + +```bash +# Setup Neon database URL +cp deployments/local/env.local.example deployments/local/.env.local +# Edit .env.local and add your Neon DATABASE_URL + +# Start services (no PostgreSQL container needed) +cd deployments/local +docker-compose up -d +``` + +## Staging Deployment + +### Prerequisites +- Kubernetes cluster access +- kubectl configured +- KUBECONFIG set +- Neon staging branch created +- GitHub Secrets configured: + - `NEON_DATABASE_URL_STAGING` + - `KUBECONFIG_STAGING` + +### Setup Secrets + +```bash +# Create Kubernetes secret +kubectl create secret generic auth-service-secrets \ + --from-literal=database-url='postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true' \ + --from-literal=jwt-secret='your-staging-jwt-secret' \ + --from-literal=jwt-refresh-secret='your-staging-refresh-secret' \ + -n staging +``` + +### Deploy + +```bash +./scripts/deploy/deploy-staging.sh +``` + +Or manually: +```bash +kubectl apply -f deployments/staging/kubernetes/ +``` + +**Note**: Migrations run automatically in CI/CD before deployment. + +## Production Deployment + +### Prerequisites +- Production Kubernetes cluster +- kubectl configured with production context +- Neon production branch created +- GitHub Secrets configured: + - `NEON_DATABASE_URL_PRODUCTION` + - `KUBECONFIG_PRODUCTION` + +### Setup Secrets + +```bash +# Create Kubernetes secret +kubectl create secret generic auth-service-secrets \ + --from-literal=database-url='postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true' \ + --from-literal=jwt-secret='your-production-jwt-secret' \ + --from-literal=jwt-refresh-secret='your-production-refresh-secret' \ + -n production +``` + +### Deploy + +```bash +./scripts/deploy/deploy-prod.sh +``` + +**Note**: Migrations run automatically in CI/CD before deployment (with approval). + +### Rollback + +```bash +kubectl rollout undo deployment/auth-service -n production +``` + +## Health Checks + +- Liveness: `GET /health/live` +- Readiness: `GET /health/ready` +- Health: `GET /health` + +## Monitoring + +- Prometheus: http://prometheus:9090 +- Grafana: http://grafana:3000 +- Traefik Dashboard: http://traefik:8080 diff --git a/docs/en/guides/development.md b/docs/en/guides/development.md new file mode 100644 index 00000000..0e0625eb --- /dev/null +++ b/docs/en/guides/development.md @@ -0,0 +1,87 @@ +# Development Guide + +## Project Structure + +``` +├── apps/ # Frontend applications +├── services/ # Backend microservices +├── packages/ # Shared libraries +├── infra/ # Infrastructure configs +├── deployments/ # Deployment configs +├── scripts/ # Automation scripts +└── docs/ # Documentation +``` + +## Development Workflow + +### 1. Create a Feature Branch + +```bash +git checkout -b feature/my-feature +``` + +### 2. Make Changes + +- Write code following TypeScript strict mode +- Add tests for new functionality +- Update documentation if needed + +### 3. Run Tests Locally + +```bash +# All tests +pnpm test + +# Specific service +pnpm --filter @goodgo/auth-service test +``` + +### 4. Lint and Format + +```bash +pnpm lint +pnpm format +``` + +### 5. Create Pull Request + +- Push your branch +- Create PR targeting `develop` +- CI/CD will run automatically + +## Adding a New Service + +1. Use the template: + ```bash + ./scripts/utils/create-service.sh my-new-service + ``` + +2. Update service configuration +3. Implement business logic +4. Add tests +5. Update documentation + +## Adding a New Package + +1. Create package in `packages/new-package` +2. Add to workspace in `pnpm-workspace.yaml` +3. Export from `index.ts` +4. Add tests +5. Document usage + +## Database Migrations + +```bash +# Create migration +cd services/auth-service +pnpm prisma migrate dev --name add_new_field + +# Apply migrations (production) +pnpm prisma migrate deploy +``` + +## Debugging + +- Use logger from `@goodgo/logger` +- Check Traefik logs: `docker logs traefik-local` +- Check service logs: `./scripts/dev/logs.sh auth-service` diff --git a/docs/en/guides/getting-started.md b/docs/en/guides/getting-started.md new file mode 100644 index 00000000..b68208ff --- /dev/null +++ b/docs/en/guides/getting-started.md @@ -0,0 +1,81 @@ +# Getting Started + +## Prerequisites + +- Node.js >= 20.0.0 +- PNPM >= 8.0.0 +- Docker & Docker Compose +- Git +- Neon account (https://neon.tech) - for database + +## Initial Setup + +1. **Clone the repository** + ```bash + git clone + cd Base + ``` + +2. **Setup Neon Database** + ```bash + # Run setup script + ./scripts/db/setup-neon.sh + + # Or manually: + # 1. Create Neon project at https://neon.tech + # 2. Create branches: main (dev), staging, production + # 3. Get connection strings + # 4. Update deployments/local/.env.local + ``` + + See [Neon Setup Guide](../../infra/databases/neon/README.md) for details. + +3. **Initialize the project** + ```bash + ./scripts/setup/init-project.sh + ``` + +4. **Start infrastructure** (Redis, Traefik - no PostgreSQL needed) + ```bash + cd deployments/local + docker-compose up -d + cd ../.. + ``` + +5. **Run database migrations** + ```bash + ./scripts/db/migrate.sh auth-service dev + ``` + +6. **Seed the database** + ```bash + ./scripts/db/seed.sh auth-service + ``` + +7. **Start all services** + ```bash + ./scripts/dev/start-all.sh + ``` + +## Access Points + +- **API Gateway**: http://localhost/api/v1 +- **Auth Service**: http://localhost:5001 +- **Web Admin**: http://admin.localhost or http://localhost:3000 +- **Web Client**: http://localhost or http://localhost:3001 +- **Traefik Dashboard**: http://localhost:8080 + +## Database + +This project uses **Neon PostgreSQL** for all environments: +- **Development**: Neon main branch +- **Staging**: Neon staging branch +- **Production**: Neon production branch + +No local PostgreSQL needed! See [Neon Setup](../../infra/databases/neon/README.md) for details. + +## Next Steps + +- Read [Development Guide](development.md) +- Check [API Documentation](../api/openapi/) +- Review [Architecture Overview](../architecture/system-design.md) diff --git a/docs/en/guides/local-development.md b/docs/en/guides/local-development.md new file mode 100644 index 00000000..69d15b2b --- /dev/null +++ b/docs/en/guides/local-development.md @@ -0,0 +1,476 @@ +# Local Development Guide + +Comprehensive guide for running and developing the project locally with real-time hot reload. + +## System Requirements + +- **Node.js**: >= 20.0.0 +- **PNPM**: >= 8.0.0 +- **Docker & Docker Compose**: Latest version +- **Git**: For cloning repository +- **Neon Account**: https://neon.tech (for database) + +## Initial Setup + +### 1. Clone Repository + +```bash +git clone +cd Base +``` + +### 2. Install Dependencies + +```bash +pnpm install +``` + +### 3. Setup Database (Neon) + +Create environment configuration file: + +```bash +cp deployments/local/env.local.example deployments/local/.env.local +``` + +Edit `.env.local` file and add your Neon DATABASE_URL: + +```bash +# Get connection string from Neon Console: https://console.neon.tech +DATABASE_URL=postgresql://user:password@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true + +# JWT Secrets (can keep defaults for dev) +JWT_SECRET=dev-jwt-secret-change-in-production +JWT_REFRESH_SECRET=dev-refresh-secret-change-in-production +``` + +**Note**: See [Neon Database Guide](neon-database.md) for detailed setup instructions. + +### 4. Run Database Migrations + +```bash +./scripts/db/migrate.sh auth-service dev +``` + +### 5. Seed Database (Optional) + +```bash +./scripts/db/seed.sh auth-service +``` + +## Ways to Run the Project + +### Method 1: Run All Services (Recommended) + +Best for full-stack development or testing the entire system: + +```bash +./scripts/dev/start-all.sh +``` + +This script will: +1. Check if Docker is running +2. Verify DATABASE_URL is configured +3. Start infrastructure (Redis, Traefik) +4. Start all services with hot reload + +**Or run manually:** + +```bash +# Step 1: Start infrastructure +cd deployments/local +docker-compose up -d +cd ../.. + +# Step 2: Start all services +pnpm dev +``` + +### Method 2: Run Specific Service + +Best when working on a single service: + +```bash +# Using script +./scripts/dev/start-service.sh auth-service + +# Or run directly +cd services/auth-service +pnpm dev +``` + +### Method 3: Run Service Groups + +```bash +# Run only backend services +pnpm --filter "./services/*" dev + +# Run only frontend apps +pnpm --filter "./apps/*" dev + +# Run specific service +pnpm --filter @goodgo/auth-service dev +``` + +### Method 4: Run With Docker Compose (Full Stack) + +```bash +cd deployments/local +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop services +docker-compose down +``` + +## Access Points + +When services are running, you can access: + +| Service | URL | Description | +|---------|-----|-------------| +| **API Gateway** | http://localhost/api/v1 | Main entry point via Traefik | +| **Auth Service** | http://localhost:5001 | Direct auth service access | +| **Auth API** | http://localhost/api/v1/auth | Auth API via gateway | +| **Web Admin** | http://admin.localhost or http://localhost:3000 | Admin dashboard | +| **Web Client** | http://localhost or http://localhost:3001 | Client web app | +| **Traefik Dashboard** | http://localhost:8080 | View routing and services | + +## Hot Reload & Live Development + +### Backend Services (TypeScript) + +Backend services use `tsx watch` or `nodemon` for automatic restart on code changes: + +```bash +# In services/auth-service/package.json +"scripts": { + "dev": "tsx watch src/index.ts" +} +``` + +**When you change:** +- `.ts` files → Service auto-restarts (1-2 seconds) +- `.env` files → Manual restart required +- `prisma/schema.prisma` → Need to run migration + +### Frontend Apps (Next.js) + +Frontend apps use Next.js Fast Refresh: + +```bash +# In apps/web-admin/package.json +"scripts": { + "dev": "next dev" +} +``` + +**When you change:** +- React components → Updates instantly (no page reload) +- CSS/Tailwind → Updates instantly +- `next.config.js` → Restart required + +### Shared Packages + +When changing shared packages (in `packages/`): + +```bash +# Packages auto-rebuild with Turbo watch mode +pnpm --filter @goodgo/logger dev +``` + +## Real-World Development Workflow + +### Setup 3 Terminals + +**Terminal 1: Run Services** +```bash +./scripts/dev/start-all.sh +# Or: pnpm dev +``` + +**Terminal 2: View Logs** +```bash +# View specific service logs +./scripts/dev/logs.sh auth-service + +# Or view Docker logs +docker logs -f redis-cache-local +docker logs -f traefik-local +``` + +**Terminal 3: Development Tasks** +```bash +# Run tests +pnpm --filter @goodgo/auth-service test --watch + +# Run migrations +./scripts/db/migrate.sh auth-service dev + +# Format code +pnpm format +``` + +## Health Checks + +### Health Endpoints + +```bash +# Check API Gateway +curl http://localhost/api/v1/health + +# Check Auth Service directly +curl http://localhost:5001/health + +# Check Redis +docker exec redis-cache-local redis-cli ping +``` + +### Traefik Dashboard + +Access http://localhost:8080 to view: +- All active routes +- Registered services +- Service health status + +## Database Development + +### Schema Changes + +```bash +# 1. Edit prisma/schema.prisma +# 2. Create and apply migration +cd services/auth-service +pnpm prisma migrate dev --name add_new_field + +# 3. Prisma Client auto-regenerates +``` + +### Reset Database (Development Only!) + +```bash +cd services/auth-service +pnpm prisma migrate reset +``` + +### View Database + +```bash +# Open Prisma Studio +cd services/auth-service +pnpm prisma studio +# Access: http://localhost:5555 +``` + +## Debugging + +### VS Code Debugging + +Create `.vscode/launch.json`: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Auth Service", + "type": "node", + "request": "launch", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["--filter", "@goodgo/auth-service", "dev"], + "skipFiles": ["/**"], + "console": "integratedTerminal" + } + ] +} +``` + +### View Detailed Logs + +```bash +# Service logs +./scripts/dev/logs.sh auth-service + +# Docker logs +docker logs -f auth-service-local +docker logs -f redis-cache-local +docker logs -f traefik-local + +# All logs +docker-compose -f deployments/local/docker-compose.yml logs -f +``` + +## Troubleshooting + +### Port Already in Use + +```bash +# Find process using port +lsof -i :5001 # Auth service +lsof -i :3000 # Web admin +lsof -i :6379 # Redis +lsof -i :80 # Traefik + +# Kill process +kill -9 +``` + +### Docker Not Running + +```bash +# Check Docker +docker info + +# Start Docker Desktop (macOS) +open -a Docker + +# Restart Docker services +docker-compose -f deployments/local/docker-compose.yml restart +``` + +### Database Connection Failed + +```bash +# Check DATABASE_URL +cat deployments/local/.env.local | grep DATABASE_URL + +# Test connection +cd services/auth-service +pnpm prisma db pull +``` + +### Module Not Found + +```bash +# Cleanup and reinstall +./scripts/utils/cleanup.sh +pnpm install + +# Or just cleanup node_modules +rm -rf node_modules +rm -rf services/*/node_modules +rm -rf apps/*/node_modules +rm -rf packages/*/node_modules +pnpm install +``` + +### Hot Reload Not Working + +```bash +# Restart service +# Press Ctrl+C to stop, then: +pnpm dev + +# Or restart Docker container +docker-compose -f deployments/local/docker-compose.yml restart auth-service +``` + +## Tips & Best Practices + +### 1. Use Turbo Cache + +Turbo cache speeds up builds: + +```bash +# First run will be slow +pnpm dev + +# Subsequent runs will be faster thanks to cache +# Cache stored in node_modules/.cache/turbo +``` + +### 2. Dev Selective Services + +No need to run everything if working on one service: + +```bash +# Run only auth-service +pnpm --filter @goodgo/auth-service dev + +# Run auth-service with dependencies +pnpm --filter @goodgo/auth-service... dev +``` + +### 3. Watch Tests + +```bash +# Run tests automatically on code changes +pnpm --filter @goodgo/auth-service test --watch +``` + +### 4. Auto-format Code + +Install Prettier extension in VS Code and enable format on save. + +### 5. Use Git Hooks + +```bash +# Pre-commit hook will auto-format and lint +git commit -m "feat: add new feature" +``` + +## Environment Variables + +### Development (.env.local) + +```bash +# Database +DATABASE_URL=postgresql://user:pass@ep-xxx.neon.tech/db?sslmode=require&pgbouncer=true + +# Redis +REDIS_HOST=redis +REDIS_PORT=6379 + +# JWT +JWT_SECRET=dev-jwt-secret +JWT_REFRESH_SECRET=dev-refresh-secret + +# Service +NODE_ENV=development +LOG_LEVEL=debug +``` + +### Override for Specific Service + +Create `.env.local` file in service directory: + +```bash +# services/auth-service/.env.local +PORT=5001 +LOG_LEVEL=debug +``` + +## Useful Commands + +```bash +# Development +pnpm dev # Run all services +pnpm build # Build all +pnpm test # Test all +pnpm lint # Lint all +pnpm format # Format code + +# Cleanup +pnpm clean # Remove build artifacts +./scripts/utils/cleanup.sh # Full cleanup + +# Database +./scripts/db/migrate.sh auth-service dev # Migration +./scripts/db/seed.sh auth-service # Seed data +./scripts/db/backup.sh auth-service # Backup + +# Docker +docker-compose -f deployments/local/docker-compose.yml up -d # Start +docker-compose -f deployments/local/docker-compose.yml down # Stop +docker-compose -f deployments/local/docker-compose.yml logs -f # Logs +docker-compose -f deployments/local/docker-compose.yml restart # Restart +``` + +## Additional Resources + +- [Getting Started](getting-started.md) - Initial setup +- [Development Guide](development.md) - Development workflow +- [Neon Database Guide](neon-database.md) - Database guide +- [Troubleshooting](troubleshooting.md) - Problem solving diff --git a/docs/en/guides/neon-database.md b/docs/en/guides/neon-database.md new file mode 100644 index 00000000..bf615bee --- /dev/null +++ b/docs/en/guides/neon-database.md @@ -0,0 +1,215 @@ +# Neon Database Guide + +This project uses [Neon PostgreSQL](https://neon.tech) for all environments. + +## Why Neon? + +- ✅ **Serverless**: No infrastructure management +- ✅ **Branching**: Separate databases for dev/staging/prod +- ✅ **Auto-scaling**: Handles traffic spikes automatically +- ✅ **Point-in-time restore**: Easy recovery from mistakes +- ✅ **Free tier**: Perfect for development +- ✅ **Connection pooling**: Built-in PgBouncer support + +## Quick Start + +### 1. Create Neon Account + +1. Sign up at https://neon.tech +2. Create a new project: `goodgo-platform` + +### 2. Create Branches + +In Neon Console, create branches: +- `main` (development) - already exists +- `staging` - create from main +- `production` - create from main + +### 3. Get Connection Strings + +For each branch, copy the connection string: +- Format: `postgresql://user:password@ep-xxx.region.neon.tech/dbname?sslmode=require` +- Add `?pgbouncer=true` for connection pooling (recommended) + +### 4. Configure Local Development + +```bash +# Create .env.local +cp deployments/local/env.local.example deployments/local/.env.local + +# Edit .env.local and add: +DATABASE_URL=postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true +``` + +### 5. Run Migrations + +```bash +./scripts/db/migrate.sh auth-service dev +``` + +## Connection String Format + +``` +postgresql://[user]:[password]@[endpoint]/[dbname]?sslmode=require&pgbouncer=true +``` + +**Parameters**: +- `sslmode=require` - Required for Neon +- `pgbouncer=true` - Enable connection pooling (recommended) + +## Environment Configuration + +### Local Development + +File: `deployments/local/.env.local` + +```bash +DATABASE_URL=postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true +``` + +### Staging + +Store in GitHub Secrets: `NEON_DATABASE_URL_STAGING` + +Or in Kubernetes: +```bash +kubectl create secret generic auth-service-secrets \ + --from-literal=database-url='postgresql://...' \ + -n staging +``` + +### Production + +Store in GitHub Secrets: `NEON_DATABASE_URL_PRODUCTION` + +Or in Kubernetes: +```bash +kubectl create secret generic auth-service-secrets \ + --from-literal=database-url='postgresql://...' \ + -n production +``` + +## Migrations + +### Development + +```bash +# Create new migration +./scripts/db/migrate.sh auth-service dev + +# This will: +# 1. Create migration file +# 2. Apply to database +# 3. Update Prisma Client +``` + +### Staging/Production + +Migrations run automatically in CI/CD: +- Before deployment to staging +- Before deployment to production (with approval) + +Manual migration: +```bash +./scripts/db/migrate.sh auth-service deploy +``` + +## Backup & Restore + +### Automatic Backups + +Neon provides automatic backups. Access via Neon Console: +- Point-in-time restore +- Branch restore +- Export data + +### Manual Backup + +```bash +./scripts/db/backup.sh auth-service +``` + +This creates a SQL dump file in `backups/` directory. + +### Restore + +```bash +# From Neon Console (recommended) +# Or using psql: +psql $DATABASE_URL < backup.sql +``` + +## Monitoring + +Monitor your databases via Neon Console: +- Connection metrics +- Query performance +- Storage usage +- Branch status + +## Troubleshooting + +### Connection Issues + +1. **Check connection string format** + - Must include `?sslmode=require` + - Verify credentials + +2. **Check IP allowlist** + - Neon may restrict IPs + - Add your IP in Neon Console + +3. **Check branch status** + - Ensure branch is active + - Check for maintenance + +### Migration Issues + +1. **DATABASE_URL not set** + ```bash + export DATABASE_URL="your-neon-url" + ``` + +2. **Schema mismatch** + ```bash + # Reset and re-migrate (dev only!) + pnpm prisma migrate reset + ``` + +3. **Connection timeout** + - Add `?pgbouncer=true` for pooling + - Check Neon console for limits + +### Performance Issues + +1. **Enable connection pooling** + - Add `?pgbouncer=true` to connection string + +2. **Check query performance** + - Use Neon Console query analyzer + - Review slow queries + +3. **Optimize indexes** + - Review Prisma schema + - Add indexes for frequent queries + +## Cost Optimization + +- **Free tier**: 0.5 GB storage, sufficient for dev +- **Staging**: Use free tier or minimal paid plan +- **Production**: Scale based on usage +- **Branching**: Free branches for testing + +## Best Practices + +1. **Always use connection pooling**: `?pgbouncer=true` +2. **Use SSL**: `?sslmode=require` +3. **Separate branches**: One per environment +4. **Regular backups**: Use Neon's automatic backups +5. **Monitor usage**: Check Neon Console regularly + +## Resources + +- [Neon Documentation](https://neon.tech/docs) +- [Neon Console](https://console.neon.tech) +- [Prisma + Neon Guide](https://neon.tech/docs/guides/prisma) diff --git a/docs/en/guides/troubleshooting.md b/docs/en/guides/troubleshooting.md new file mode 100644 index 00000000..78b5398e --- /dev/null +++ b/docs/en/guides/troubleshooting.md @@ -0,0 +1,57 @@ +# Troubleshooting Guide + +## Common Issues + +### Database Connection Failed + +**Symptoms**: Service can't connect to database + +**Solutions**: +1. Check if PostgreSQL is running: `docker ps` +2. Verify DATABASE_URL in .env +3. Check network connectivity: `docker network ls` +4. Review logs: `docker logs postgres-auth-local` + +### Port Already in Use + +**Symptoms**: Service fails to start with port error + +**Solutions**: +1. Find process using port: `lsof -i :5001` +2. Kill process or change PORT in .env +3. Check docker-compose for port conflicts + +### Prisma Client Not Generated + +**Symptoms**: Import errors for Prisma Client + +**Solutions**: +```bash +cd services/auth-service +pnpm prisma generate +``` + +### Build Failures + +**Symptoms**: TypeScript or build errors + +**Solutions**: +1. Clean build artifacts: `./scripts/utils/cleanup.sh` +2. Reinstall dependencies: `pnpm install` +3. Check TypeScript errors: `pnpm typecheck` + +### Traefik Not Routing + +**Symptoms**: 404 errors from Traefik + +**Solutions**: +1. Check Traefik dashboard: http://localhost:8080 +2. Verify service labels in docker-compose +3. Check routes.yml configuration +4. Review Traefik logs: `docker logs traefik-local` + +## Getting Help + +1. Check service logs: `./scripts/dev/logs.sh ` +2. Review GitHub Issues +3. Contact team lead diff --git a/docs/en/onboarding/new-developer-guide.md b/docs/en/onboarding/new-developer-guide.md new file mode 100644 index 00000000..f4a7f4dd --- /dev/null +++ b/docs/en/onboarding/new-developer-guide.md @@ -0,0 +1,89 @@ +# New Developer Guide + +Welcome to the GoodGo Microservices Platform team! + +## First Day Checklist + +- [ ] Access to GitHub repository +- [ ] Access to development environment +- [ ] Docker installed and running +- [ ] Node.js and PNPM installed +- [ ] IDE configured (VS Code recommended) +- [ ] Read this guide + +## Setup Your Development Environment + +1. **Clone the repository** + ```bash + git clone + cd Base + ``` + +2. **Run initialization script** + ```bash + ./scripts/setup/init-project.sh + ``` + +3. **Start local infrastructure** + ```bash + cd deployments/local + docker-compose up -d + ``` + +4. **Verify setup** + - Check Traefik: http://localhost:8080 + - Check API: http://localhost/api/v1/health + +## Development Tools + +### Recommended VS Code Extensions + +- ESLint +- Prettier +- Prisma +- Docker +- GitLens + +### Useful Commands + +```bash +# Start all services +./scripts/dev/start-all.sh + +# Start specific service +./scripts/dev/start-service.sh auth-service + +# View logs +./scripts/dev/logs.sh auth-service + +# Run migrations +./scripts/db/migrate.sh auth-service dev + +# Run tests +pnpm test +``` + +## Code Standards + +- **TypeScript**: Strict mode enabled +- **Linting**: ESLint with shared config +- **Formatting**: Prettier +- **Commits**: Conventional Commits format +- **Tests**: Minimum 80% coverage + +## Getting Help + +- Check [Documentation](../guides/) +- Ask in team Slack channel +- Review existing code examples +- Pair with senior developer + +## Next Steps + +1. Pick a small task from backlog +2. Create feature branch +3. Implement and test +4. Create pull request +5. Get code review + +Good luck! 🚀 diff --git a/docs/en/runbooks/incident-response.md b/docs/en/runbooks/incident-response.md new file mode 100644 index 00000000..9af6b5b4 --- /dev/null +++ b/docs/en/runbooks/incident-response.md @@ -0,0 +1,65 @@ +# Incident Response Runbook + +## Severity Levels + +- **P0 - Critical**: Service completely down, data loss +- **P1 - High**: Major functionality broken, affecting many users +- **P2 - Medium**: Minor functionality broken, workaround available +- **P3 - Low**: Cosmetic issues, no user impact + +## Response Process + +### 1. Acknowledge Incident + +- Identify severity level +- Notify team via Slack/email +- Create incident ticket + +### 2. Investigate + +- Check service health endpoints +- Review logs: `./scripts/dev/logs.sh ` +- Check monitoring dashboards (Grafana) +- Review recent deployments + +### 3. Mitigate + +- Apply quick fixes if available +- Rollback if recent deployment caused issue +- Scale up if resource constraint + +### 4. Resolve + +- Implement permanent fix +- Verify resolution +- Update documentation + +### 5. Post-Mortem + +- Document incident +- Identify root cause +- Create action items +- Update runbooks + +## Common Scenarios + +### Service Down + +1. Check Kubernetes pods: `kubectl get pods -n ` +2. Check pod logs: `kubectl logs -n ` +3. Restart service: `kubectl rollout restart deployment/ -n ` +4. If persistent, rollback: `kubectl rollout undo deployment/ -n ` + +### Database Issues + +1. Check database connectivity +2. Review slow queries +3. Check connection pool +4. Scale database if needed + +### High Error Rate + +1. Check error logs +2. Review recent changes +3. Check external dependencies +4. Implement circuit breaker if needed diff --git a/docs/en/runbooks/rollback-procedure.md b/docs/en/runbooks/rollback-procedure.md new file mode 100644 index 00000000..d0ecdb1a --- /dev/null +++ b/docs/en/runbooks/rollback-procedure.md @@ -0,0 +1,71 @@ +# Rollback Procedure + +## When to Rollback + +- Service is down or unstable +- Critical bugs introduced +- Performance degradation +- Data corruption risk + +## Rollback Steps + +### Kubernetes Rollback + +1. **Identify current version** + ```bash + kubectl get deployment auth-service -n production -o jsonpath='{.spec.template.spec.containers[0].image}' + ``` + +2. **Rollback to previous version** + ```bash + kubectl rollout undo deployment/auth-service -n production + ``` + +3. **Verify rollback** + ```bash + kubectl rollout status deployment/auth-service -n production + ``` + +4. **Check service health** + ```bash + curl https://api.goodgo.vn/health + ``` + +### Database Migration Rollback + +**Note**: Prisma doesn't support automatic rollback. Create a new migration to reverse changes. + +1. Create reverse migration: + ```bash + cd services/auth-service + pnpm prisma migrate dev --name rollback_previous_change + ``` + +2. Apply reverse migration: + ```bash + pnpm prisma migrate deploy + ``` + +### Docker Compose Rollback + +1. Stop current containers: + ```bash + docker-compose down + ``` + +2. Checkout previous version: + ```bash + git checkout + ``` + +3. Rebuild and start: + ```bash + docker-compose up -d --build + ``` + +## Post-Rollback + +1. Verify functionality +2. Monitor metrics +3. Document rollback reason +4. Plan fix for next deployment diff --git a/docs/vi/api/openapi/auth-service.yaml b/docs/vi/api/openapi/auth-service.yaml new file mode 100644 index 00000000..9a3c1606 --- /dev/null +++ b/docs/vi/api/openapi/auth-service.yaml @@ -0,0 +1,102 @@ +openapi: 3.0.0 +info: + title: Auth Service API + version: 1.0.0 + description: Authentication and Authorization Service API + +servers: + - url: http://localhost/api/v1 + description: Local development + - url: https://api.goodgo.vn/api/v1 + description: Production + +paths: + /auth/register: + post: + summary: Register new user + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + format: email + password: + type: string + minLength: 6 + confirmPassword: + type: string + responses: + '201': + description: User registered successfully + '400': + description: Validation error + + /auth/login: + post: + summary: Login user + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + format: email + password: + type: string + responses: + '200': + description: Login successful + '401': + description: Invalid credentials + + /auth/logout: + post: + summary: Logout user + security: + - bearerAuth: [] + responses: + '200': + description: Logout successful + + /auth/refresh: + post: + summary: Refresh access token + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + refreshToken: + type: string + responses: + '200': + description: Token refreshed + '401': + description: Invalid refresh token + + /users/me: + get: + summary: Get current user + security: + - bearerAuth: [] + responses: + '200': + description: User information + '401': + description: Unauthorized + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/docs/vi/architecture/service-communication.md b/docs/vi/architecture/service-communication.md new file mode 100644 index 00000000..3028fa06 --- /dev/null +++ b/docs/vi/architecture/service-communication.md @@ -0,0 +1,58 @@ +# Giao Tiếp Giữa Các Service + +## Các Mẫu Giao Tiếp + +### Giao Tiếp Đồng Bộ (HTTP/REST) + +Các service giao tiếp đồng bộ qua HTTP REST APIs thông qua Traefik API Gateway. + +**Ví dụ:** +```typescript +// Web App -> Auth Service +const response = await fetch('http://api.goodgo.vn/api/v1/auth/login', { + method: 'POST', + body: JSON.stringify({ email, password }), +}); +``` + +### Giao Tiếp Service-to-Service + +Các service có thể giao tiếp trực tiếp qua mạng nội bộ: + +```typescript +// Auth Service -> Notification Service (tương lai) +const response = await fetch('http://notification-service:5003/api/v1/notifications', { + method: 'POST', + headers: { 'X-Service-Auth': process.env.INTERNAL_API_KEY }, + body: JSON.stringify({ userId, message }), +}); +``` + +## API Gateway Routing + +Traefik định tuyến requests dựa trên: +- Host header (`api.goodgo.vn`) +- Path prefix (`/api/v1/auth`) + +## Xử Lý Lỗi + +Tất cả services tuân theo định dạng error response nhất quán: + +```json +{ + "success": false, + "error": { + "code": "AUTH_001", + "message": "Invalid credentials", + "details": {} + }, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +## Retry và Circuit Breaker + +Triển khai trong tương lai: +- Exponential backoff cho retries +- Circuit breaker pattern cho fault tolerance +- Fallback mechanisms diff --git a/docs/vi/architecture/system-design.md b/docs/vi/architecture/system-design.md new file mode 100644 index 00000000..ec0075f5 --- /dev/null +++ b/docs/vi/architecture/system-design.md @@ -0,0 +1,81 @@ +# Thiết Kế Hệ Thống + +## Tổng Quan + +GoodGo Microservices Platform được xây dựng sử dụng kiến trúc microservices với các nguyên tắc sau: + +- **Độc Lập Service**: Mỗi service có database riêng và có thể deploy độc lập +- **API Gateway**: Traefik xử lý routing, load balancing, và các concerns xuyên suốt +- **Shared Libraries**: Chức năng chung được trích xuất vào shared packages +- **Infrastructure as Code**: Tất cả cấu hình infrastructure đều được version +- **Observability**: Đầy đủ khả năng monitoring, logging, và tracing + +## Sơ Đồ Kiến Trúc + +``` +┌─────────────┐ ┌─────────────┐ +│ Web App │ │ Mobile App │ +│ (Next.js) │ │ (React Native) +└──────┬──────┘ └──────┬──────┘ + │ │ + └──────────┬────────┘ + │ + ┌────────▼────────┐ + │ Traefik │ + │ (API Gateway) │ + └────────┬─────────┘ + │ + ┌─────────────┼─────────────┐ + │ │ │ +┌───▼────┐ ┌───▼────┐ ┌───▼────┐ +│ Auth │ │ Future │ │ Future │ +│Service │ │Service │ │Service │ +└───┬────┘ └───┬────┘ └───┬────┘ + │ │ │ + └────────────┼────────────┘ + │ + ┌────────────┼────────────┐ + │ │ │ +┌───▼────┐ ┌───▼────┐ ┌───▼────┐ +│Postgres│ │ Redis │ │Prometheus│ +└────────┘ └────────┘ └─────────┘ +``` + +## Các Thành Phần + +### Frontend Layer +- **Web App**: Ứng dụng Next.js với App Router +- **Mobile App**: Ứng dụng React Native + +### API Gateway +- **Traefik**: Reverse proxy, load balancer, SSL termination + +### Services Layer +- **Auth Service**: Xác thực và phân quyền +- **Future Services**: Payment, Order, Notification, v.v. + +### Infrastructure Layer +- **PostgreSQL**: Database chính +- **Redis**: Caching và session storage +- **Prometheus**: Thu thập metrics +- **Grafana**: Hiển thị metrics +- **Loki**: Tập hợp logs + +## Các Mẫu Giao Tiếp + +- **Đồng Bộ**: HTTP/REST cho các mẫu request-response +- **Bất Đồng Bộ**: Message queues (triển khai trong tương lai) +- **Service Discovery**: Docker networking và Kubernetes DNS + +## Quản Lý Dữ Liệu + +- **Database per Service**: Mỗi service sở hữu dữ liệu của mình +- **API Composition**: Services expose APIs để truy cập dữ liệu +- **Event Sourcing**: Xem xét trong tương lai cho audit trails + +## Bảo Mật + +- **Authentication**: JWT tokens với refresh token rotation +- **Authorization**: Role-based access control (RBAC) +- **Network Security**: TLS/SSL, rate limiting, CORS +- **Secrets Management**: Environment variables, Kubernetes secrets diff --git a/docs/vi/guides/deployment.md b/docs/vi/guides/deployment.md new file mode 100644 index 00000000..5d8d55b4 --- /dev/null +++ b/docs/vi/guides/deployment.md @@ -0,0 +1,106 @@ +# Hướng Dẫn Deployment + +## Thiết Lập Database (Neon) + +Tất cả môi trường sử dụng **Neon PostgreSQL**. Thiết lập một lần trước khi deploy: + +1. Tạo Neon project tại https://neon.tech +2. Tạo branches: `main` (dev), `staging`, `production` +3. Lấy connection strings cho mỗi branch +4. Cấu hình trong environment variables (xem bên dưới) + +Xem [Hướng Dẫn Thiết Lập Neon](../../infra/databases/neon/README.md) để biết chi tiết. + +## Local Deployment + +```bash +# Setup Neon database URL +cp deployments/local/env.local.example deployments/local/.env.local +# Chỉnh sửa .env.local và thêm Neon DATABASE_URL của bạn + +# Khởi động services (không cần PostgreSQL container) +cd deployments/local +docker-compose up -d +``` + +## Staging Deployment + +### Yêu Cầu +- Quyền truy cập Kubernetes cluster +- kubectl đã cấu hình +- KUBECONFIG đã set +- Neon staging branch đã tạo +- GitHub Secrets đã cấu hình: + - `NEON_DATABASE_URL_STAGING` + - `KUBECONFIG_STAGING` + +### Thiết Lập Secrets + +```bash +# Tạo Kubernetes secret +kubectl create secret generic auth-service-secrets \ + --from-literal=database-url='postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true' \ + --from-literal=jwt-secret='your-staging-jwt-secret' \ + --from-literal=jwt-refresh-secret='your-staging-refresh-secret' \ + -n staging +``` + +### Deploy + +```bash +./scripts/deploy/deploy-staging.sh +``` + +Hoặc thủ công: +```bash +kubectl apply -f deployments/staging/kubernetes/ +``` + +**Lưu ý**: Migrations chạy tự động trong CI/CD trước khi deployment. + +## Production Deployment + +### Yêu Cầu +- Production Kubernetes cluster +- kubectl đã cấu hình với production context +- Neon production branch đã tạo +- GitHub Secrets đã cấu hình: + - `NEON_DATABASE_URL_PRODUCTION` + - `KUBECONFIG_PRODUCTION` + +### Thiết Lập Secrets + +```bash +# Tạo Kubernetes secret +kubectl create secret generic auth-service-secrets \ + --from-literal=database-url='postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true' \ + --from-literal=jwt-secret='your-production-jwt-secret' \ + --from-literal=jwt-refresh-secret='your-production-refresh-secret' \ + -n production +``` + +### Deploy + +```bash +./scripts/deploy/deploy-prod.sh +``` + +**Lưu ý**: Migrations chạy tự động trong CI/CD trước khi deployment (cần approval). + +### Rollback + +```bash +kubectl rollout undo deployment/auth-service -n production +``` + +## Health Checks + +- Liveness: `GET /health/live` +- Readiness: `GET /health/ready` +- Health: `GET /health` + +## Monitoring + +- Prometheus: http://prometheus:9090 +- Grafana: http://grafana:3000 +- Traefik Dashboard: http://traefik:8080 diff --git a/docs/vi/guides/development.md b/docs/vi/guides/development.md new file mode 100644 index 00000000..3bd5b56c --- /dev/null +++ b/docs/vi/guides/development.md @@ -0,0 +1,87 @@ +# Hướng Dẫn Development + +## Cấu Trúc Dự Án + +``` +├── apps/ # Frontend applications +├── services/ # Backend microservices +├── packages/ # Shared libraries +├── infra/ # Infrastructure configs +├── deployments/ # Deployment configs +├── scripts/ # Automation scripts +└── docs/ # Documentation +``` + +## Quy Trình Development + +### 1. Tạo Feature Branch + +```bash +git checkout -b feature/my-feature +``` + +### 2. Thực Hiện Thay Đổi + +- Viết code tuân theo TypeScript strict mode +- Thêm tests cho chức năng mới +- Cập nhật tài liệu nếu cần + +### 3. Chạy Tests Locally + +```bash +# Tất cả tests +pnpm test + +# Service cụ thể +pnpm --filter @goodgo/auth-service test +``` + +### 4. Lint và Format + +```bash +pnpm lint +pnpm format +``` + +### 5. Tạo Pull Request + +- Push branch của bạn +- Tạo PR target `develop` +- CI/CD sẽ chạy tự động + +## Thêm Service Mới + +1. Sử dụng template: + ```bash + ./scripts/utils/create-service.sh my-new-service + ``` + +2. Cập nhật cấu hình service +3. Implement business logic +4. Thêm tests +5. Cập nhật tài liệu + +## Thêm Package Mới + +1. Tạo package trong `packages/new-package` +2. Thêm vào workspace trong `pnpm-workspace.yaml` +3. Export từ `index.ts` +4. Thêm tests +5. Ghi lại cách sử dụng + +## Database Migrations + +```bash +# Tạo migration +cd services/auth-service +pnpm prisma migrate dev --name add_new_field + +# Áp dụng migrations (production) +pnpm prisma migrate deploy +``` + +## Debugging + +- Sử dụng logger từ `@goodgo/logger` +- Kiểm tra Traefik logs: `docker logs traefik-local` +- Kiểm tra service logs: `./scripts/dev/logs.sh auth-service` diff --git a/docs/vi/guides/getting-started.md b/docs/vi/guides/getting-started.md new file mode 100644 index 00000000..445a08b4 --- /dev/null +++ b/docs/vi/guides/getting-started.md @@ -0,0 +1,81 @@ +# Bắt Đầu + +## Yêu Cầu + +- Node.js >= 20.0.0 +- PNPM >= 8.0.0 +- Docker & Docker Compose +- Git +- Tài khoản Neon (https://neon.tech) - cho database + +## Thiết Lập Ban Đầu + +1. **Clone repository** + ```bash + git clone + cd Base + ``` + +2. **Thiết Lập Neon Database** + ```bash + # Chạy script setup + ./scripts/db/setup-neon.sh + + # Hoặc thủ công: + # 1. Tạo Neon project tại https://neon.tech + # 2. Tạo branches: main (dev), staging, production + # 3. Lấy connection strings + # 4. Cập nhật deployments/local/.env.local + ``` + + Xem [Hướng Dẫn Thiết Lập Neon](../../infra/databases/neon/README.md) để biết chi tiết. + +3. **Khởi tạo project** + ```bash + ./scripts/setup/init-project.sh + ``` + +4. **Khởi động infrastructure** (Redis, Traefik - không cần PostgreSQL) + ```bash + cd deployments/local + docker-compose up -d + cd ../.. + ``` + +5. **Chạy database migrations** + ```bash + ./scripts/db/migrate.sh auth-service dev + ``` + +6. **Seed database** + ```bash + ./scripts/db/seed.sh auth-service + ``` + +7. **Khởi động tất cả services** + ```bash + ./scripts/dev/start-all.sh + ``` + +## Điểm Truy Cập + +- **API Gateway**: http://localhost/api/v1 +- **Auth Service**: http://localhost:5001 +- **Web Admin**: http://admin.localhost hoặc http://localhost:3000 +- **Web Client**: http://localhost hoặc http://localhost:3001 +- **Traefik Dashboard**: http://localhost:8080 + +## Database + +Project này sử dụng **Neon PostgreSQL** cho tất cả môi trường: +- **Development**: Neon main branch +- **Staging**: Neon staging branch +- **Production**: Neon production branch + +Không cần PostgreSQL local! Xem [Thiết Lập Neon](../../infra/databases/neon/README.md) để biết chi tiết. + +## Các Bước Tiếp Theo + +- Đọc [Hướng Dẫn Development](development.md) +- Xem [Tài Liệu API](../api/openapi/) +- Xem lại [Tổng Quan Kiến Trúc](../architecture/system-design.md) diff --git a/docs/vi/guides/local-development.md b/docs/vi/guides/local-development.md new file mode 100644 index 00000000..5a597dbe --- /dev/null +++ b/docs/vi/guides/local-development.md @@ -0,0 +1,741 @@ +# Hướng Dẫn Development Local + +Hướng dẫn chi tiết cách chạy và phát triển dự án trên máy local với hot reload real-time. + +## Yêu Cầu Hệ Thống + +- **Node.js**: >= 20.0.0 +- **PNPM**: >= 8.0.0 +- **Docker & Docker Compose**: Phiên bản mới nhất +- **Git**: Để clone repository +- **Tài khoản Neon**: https://neon.tech (cho database) + +## Thiết Lập Ban Đầu + +### 1. Clone Repository + +```bash +git clone +cd Base +``` + +### 2. Cài Đặt Dependencies + +```bash +pnpm install +``` + +### 3. Thiết Lập Environment Variables + +Dự án sử dụng **Hybrid Environment Configuration**: +- **Shared configs** (JWT secrets, Redis): `deployments/local/.env.local` +- **Service-specific configs** (DATABASE_URL, PORT): `services//.env.local` + +#### 3.1. Tạo Shared Environment File + +```bash +cp deployments/local/env.local.example deployments/local/.env.local +``` + +File này chứa configs dùng chung cho tất cả services: + +```bash +# JWT Secrets - MUST be same across all services +JWT_SECRET=dev-jwt-secret-change-in-production-min-32-chars +JWT_REFRESH_SECRET=dev-refresh-secret-change-in-production-min-32-chars + +# Redis (Docker) +REDIS_HOST=redis +REDIS_PORT=6379 + +# Common configs +NODE_ENV=development +LOG_LEVEL=debug +CORS_ORIGIN=http://localhost:3000,http://localhost:3001 +``` + +#### 3.2. Tạo Service-Specific Environment File + +Mỗi service cần file `.env.local` riêng cho DATABASE_URL và configs cụ thể: + +```bash +# Ví dụ: Auth Service +cp services/auth-service/env.local.example services/auth-service/.env.local +``` + +Chỉnh sửa `services/auth-service/.env.local`: + +```bash +# Database riêng cho auth-service +DATABASE_URL=postgresql://user:password@ep-xxx.region.neon.tech/goodgo_auth_dev?sslmode=require&pgbouncer=true + +# Service configs +PORT=5001 +SERVICE_NAME=auth-service + +# Redis override (native dev - Redis in Docker) +REDIS_HOST=localhost +``` + +**Lưu ý**: +- Mỗi service có **database riêng** (microservices pattern) +- JWT secrets **phải giống nhau** để services verify tokens của nhau +- Xem [Hướng Dẫn Neon Database](neon-database.md) để tạo databases + +### 4. Chạy Database Migrations + +```bash +./scripts/db/migrate.sh auth-service dev +``` + +### 5. Seed Database (Tùy chọn) + +```bash +./scripts/db/seed.sh auth-service +``` + +## Các Cách Chạy Dự Án + +### Cách 1: Chạy Tất Cả Services (Khuyến nghị) + +Cách này phù hợp khi bạn làm full-stack hoặc cần test toàn bộ hệ thống: + +```bash +./scripts/dev/start-all.sh +``` + +Script này sẽ: +1. Kiểm tra Docker đang chạy +2. Kiểm tra DATABASE_URL đã được cấu hình +3. Khởi động infrastructure (Redis, Traefik) +4. Khởi động tất cả services với hot reload + +**Hoặc chạy thủ công:** + +```bash +# Bước 1: Khởi động infrastructure +cd deployments/local +docker-compose up -d +cd ../.. + +# Bước 2: Khởi động tất cả services +pnpm dev +``` + +### Cách 2: Chạy Service Cụ Thể + +Cách này phù hợp khi bạn chỉ làm việc với 1 service: + +```bash +# Sử dụng script +./scripts/dev/start-service.sh auth-service + +# Hoặc chạy trực tiếp +cd services/auth-service +pnpm dev +``` + +### Cách 3: Hybrid - Native + Docker (Linh hoạt nhất) + +Cách này kết hợp tốt nhất của cả hai thế giới: Infrastructure chạy Docker, services đang dev chạy native với hot reload nhanh. + +**Phù hợp khi:** +- Làm việc với 1-2 services cụ thể +- Cần services khác chạy background +- Muốn hot reload nhanh cho service đang dev +- Tiết kiệm tài nguyên máy + +**Setup:** + +```bash +# Bước 1: Khởi động infrastructure (Redis, Traefik) +cd deployments/local +docker-compose up -d redis traefik +cd ../.. + +# Bước 2: Chạy service đang dev với native (hot reload nhanh) +pnpm --filter @goodgo/auth-service dev + +# Bước 3: (Tùy chọn) Chạy services khác trong Docker nếu cần +docker-compose -f deployments/local/docker-compose.yml up -d user-service payment-service +``` + +**Ví dụ workflow thực tế:** + +```bash +# Scenario 1: Chỉ dev auth-service +cd deployments/local && docker-compose up -d redis traefik && cd ../.. +pnpm --filter @goodgo/auth-service dev + +# Scenario 2: Dev auth-service + cần web-admin để test +cd deployments/local && docker-compose up -d redis traefik && cd ../.. +pnpm --filter @goodgo/auth-service dev & +pnpm --filter @goodgo/web-admin dev + +# Scenario 3: Dev frontend, backend chạy Docker +cd deployments/local && docker-compose up -d redis traefik auth-service && cd ../.. +pnpm --filter @goodgo/web-admin dev +``` + +**Lợi ích:** +- ⚡ Hot reload cực nhanh (1-2s) cho service đang dev +- 💻 Tiết kiệm RAM - chỉ chạy Docker cho services cần thiết +- 🐛 Debug dễ dàng - attach debugger trực tiếp +- 🎯 Linh hoạt - chọn service nào chạy native, service nào chạy Docker + +### Cách 4: Chạy Nhóm Services + +```bash +# Chỉ chạy backend services +pnpm --filter "./services/*" dev + +# Chỉ chạy frontend apps +pnpm --filter "./apps/*" dev + +# Chạy service cụ thể với dependencies +pnpm --filter @goodgo/auth-service... dev +``` + +### Cách 5: Chạy Với Docker Compose (Full Stack) + +```bash +cd deployments/local +docker-compose up -d + +# Xem logs +docker-compose logs -f + +# Dừng services +docker-compose down +``` + +**Lưu ý:** Dockerfile hiện tại là production build, không có hot reload. Phù hợp để test môi trường giống production. + +## So Sánh Các Phương Án + +| Tiêu chí | Cách 1: All Native | Cách 3: Hybrid | Cách 5: Full Docker | +|----------|-------------------|----------------|---------------------| +| **Hot Reload** | ⚡ Cực nhanh (1-2s) | ⚡ Nhanh cho service native | ❌ Không có (production build) | +| **RAM Usage** | 💚 Thấp (~1-2GB) | 💛 Trung bình (~2-3GB) | 🔴 Cao (~3-5GB) | +| **Debug** | ✅ Dễ nhất | ✅ Dễ (native services) | ⚠️ Khó hơn (qua container) | +| **Setup** | 🟢 Đơn giản | 🟡 Trung bình | 🟢 Đơn giản | +| **Giống Production** | ⚠️ Khác biệt | 🟡 Một phần | ✅ Gần giống nhất | +| **Khi nào dùng** | Dev hàng ngày | Dev 1-2 services | Test integration/deployment | + +**Khuyến nghị:** +- 🎯 **90% thời gian**: Dùng **Cách 1** (All Native) - nhanh nhất, tiện nhất +- 🔧 **Khi cần linh hoạt**: Dùng **Cách 3** (Hybrid) - chọn service nào chạy native +- 🐳 **Test production-like**: Dùng **Cách 5** (Full Docker) - test networking, deployment + +## Điểm Truy Cập + +Khi các services đang chạy, bạn có thể truy cập: + +| Service | URL | Mô tả | +|---------|-----|-------| +| **API Gateway** | http://localhost/api/v1 | Điểm truy cập chính qua Traefik | +| **Auth Service** | http://localhost:5001 | Truy cập trực tiếp auth service | +| **Auth API** | http://localhost/api/v1/auth | Auth API qua gateway | +| **Web Admin** | http://admin.localhost hoặc http://localhost:3000 | Admin dashboard | +| **Web Client** | http://localhost hoặc http://localhost:3001 | Client web app | +| **Traefik Dashboard** | http://localhost:8080 | Xem routing và services | + +## Hot Reload & Live Development + +### Backend Services (TypeScript) + +Backend services sử dụng `tsx watch` hoặc `nodemon` để tự động restart khi code thay đổi: + +```bash +# Trong services/auth-service/package.json +"scripts": { + "dev": "tsx watch src/index.ts" +} +``` + +**Khi bạn thay đổi:** +- `.ts` files → Service tự động restart (1-2 giây) +- `.env` files → Cần restart thủ công +- `prisma/schema.prisma` → Cần chạy migration + +### Frontend Apps (Next.js) + +Frontend apps sử dụng Next.js Fast Refresh: + +```bash +# Trong apps/web-admin/package.json +"scripts": { + "dev": "next dev" +} +``` + +**Khi bạn thay đổi:** +- React components → Cập nhật ngay lập tức (không reload page) +- CSS/Tailwind → Cập nhật ngay lập tức +- `next.config.js` → Cần restart + +### Shared Packages + +Khi thay đổi shared packages (trong `packages/`): + +```bash +# Packages tự động rebuild với Turbo watch mode +pnpm --filter @goodgo/logger dev +``` + +## Workflow Development Thực Tế + +### Setup 3 Terminals (Khuyến nghị) + +#### Option A: Development Hàng Ngày (All Native) + +**Terminal 1: Chạy Services** +```bash +./scripts/dev/start-all.sh +# Hoặc: pnpm dev +``` + +**Terminal 2: Watch Tests** +```bash +# Auto-run tests khi code thay đổi +pnpm --filter @goodgo/auth-service test --watch +``` + +**Terminal 3: Development Tasks** +```bash +# Prisma Studio +pnpm --filter @goodgo/auth-service prisma studio + +# Xem logs +./scripts/dev/logs.sh auth-service + +# Migrations +./scripts/db/migrate.sh auth-service dev +``` + +#### Option B: Hybrid Development (Selective Services) + +**Terminal 1: Infrastructure + Service đang dev** +```bash +# Start infrastructure +cd deployments/local && docker-compose up -d redis traefik && cd ../.. + +# Dev service cụ thể với hot reload +pnpm --filter @goodgo/auth-service dev +``` + +**Terminal 2: Frontend (nếu cần)** +```bash +pnpm --filter @goodgo/web-admin dev +``` + +**Terminal 3: Tools & Logs** +```bash +# Watch tests +pnpm --filter @goodgo/auth-service test --watch + +# Xem Docker logs +docker logs -f redis-cache-local + +# Quick commands +pnpm format +``` + +### Workflow Theo Use Case + +#### Use Case 1: Dev Backend Service + +```bash +# Terminal 1 +cd deployments/local && docker-compose up -d redis traefik && cd ../.. +pnpm --filter @goodgo/auth-service dev + +# Terminal 2 +pnpm --filter @goodgo/auth-service test --watch + +# Terminal 3 +pnpm --filter @goodgo/auth-service prisma studio +``` + +#### Use Case 2: Dev Frontend + Backend + +```bash +# Terminal 1: Backend +pnpm --filter @goodgo/auth-service dev + +# Terminal 2: Frontend +pnpm --filter @goodgo/web-admin dev + +# Terminal 3: Infrastructure +cd deployments/local && docker-compose up -d redis traefik +``` + +#### Use Case 3: Full Stack Development + +```bash +# Terminal 1: All services +./scripts/dev/start-all.sh + +# Terminal 2: Watch tests +pnpm test --watch + +# Terminal 3: Tools +# Prisma Studio, logs, migrations, etc. +``` + +## Kiểm Tra Health + +### Health Endpoints + +```bash +# Kiểm tra API Gateway +curl http://localhost/api/v1/health + +# Kiểm tra Auth Service trực tiếp +curl http://localhost:5001/health + +# Kiểm tra Redis +docker exec redis-cache-local redis-cli ping +``` + +### Traefik Dashboard + +Truy cập http://localhost:8080 để xem: +- Tất cả routes đang hoạt động +- Services đã đăng ký +- Health status của services + +## Database Development + +### Thay Đổi Schema + +```bash +# 1. Chỉnh sửa prisma/schema.prisma +# 2. Tạo và áp dụng migration +cd services/auth-service +pnpm prisma migrate dev --name add_new_field + +# 3. Prisma Client sẽ tự động regenerate +``` + +### Reset Database (Development Only!) + +```bash +cd services/auth-service +pnpm prisma migrate reset +``` + +### Xem Database + +```bash +# Mở Prisma Studio +cd services/auth-service +pnpm prisma studio +# Truy cập: http://localhost:5555 +``` + +## Debugging + +### VS Code Debugging + +Tạo file `.vscode/launch.json`: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Auth Service", + "type": "node", + "request": "launch", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["--filter", "@goodgo/auth-service", "dev"], + "skipFiles": ["/**"], + "console": "integratedTerminal" + } + ] +} +``` + +### Xem Logs Chi Tiết + +```bash +# Service logs +./scripts/dev/logs.sh auth-service + +# Docker logs +docker logs -f auth-service-local +docker logs -f redis-cache-local +docker logs -f traefik-local + +# Tất cả logs +docker-compose -f deployments/local/docker-compose.yml logs -f +``` + +## Troubleshooting + +### Port Đã Được Sử Dụng + +```bash +# Tìm process đang dùng port +lsof -i :5001 # Auth service +lsof -i :3000 # Web admin +lsof -i :6379 # Redis +lsof -i :80 # Traefik + +# Kill process +kill -9 +``` + +### Docker Không Chạy + +```bash +# Kiểm tra Docker +docker info + +# Khởi động Docker Desktop (macOS) +open -a Docker + +# Restart Docker services +docker-compose -f deployments/local/docker-compose.yml restart +``` + +### Database Connection Failed + +```bash +# Kiểm tra DATABASE_URL trong service-specific env +cat services/auth-service/.env.local | grep DATABASE_URL + +# Nếu chưa có file .env.local, tạo từ example +cp services/auth-service/env.local.example services/auth-service/.env.local + +# Chỉnh sửa DATABASE_URL với connection string từ Neon +# DATABASE_URL=postgresql://user:pass@ep-xxx.neon.tech/goodgo_auth_dev?sslmode=require&pgbouncer=true + +# Test connection +cd services/auth-service +pnpm prisma db pull +``` + +**Lưu ý**: Mỗi service cần file `.env.local` riêng với DATABASE_URL của service đó. + +### Module Not Found + +```bash +# Cleanup và reinstall +./scripts/utils/cleanup.sh +pnpm install + +# Hoặc chỉ cleanup node_modules +rm -rf node_modules +rm -rf services/*/node_modules +rm -rf apps/*/node_modules +rm -rf packages/*/node_modules +pnpm install +``` + +### Hot Reload Không Hoạt Động + +```bash +# Restart service +# Ctrl+C để dừng, sau đó: +pnpm dev + +# Hoặc restart Docker container +docker-compose -f deployments/local/docker-compose.yml restart auth-service +``` + +## Tips & Best Practices + +### 1. Sử Dụng Turbo Cache + +Turbo cache giúp build nhanh hơn: + +```bash +# Lần đầu chạy sẽ chậm +pnpm dev + +# Các lần sau sẽ nhanh hơn nhờ cache +# Cache được lưu trong node_modules/.cache/turbo +``` + +### 2. Dev Selective Services + +Không cần chạy tất cả nếu chỉ làm 1 service: + +```bash +# Chỉ chạy auth-service +pnpm --filter @goodgo/auth-service dev + +# Chạy auth-service và dependencies +pnpm --filter @goodgo/auth-service... dev +``` + +### 3. Watch Tests + +```bash +# Chạy tests tự động khi code thay đổi +pnpm --filter @goodgo/auth-service test --watch +``` + +### 4. Format Code Tự Động + +Cài đặt Prettier extension trong VS Code và bật format on save. + +### 5. Sử Dụng Git Hooks + +```bash +# Pre-commit hook sẽ tự động format và lint +git commit -m "feat: add new feature" +``` + +### 6. Hybrid Development (Best of Both Worlds) + +Kết hợp Docker và Native để tối ưu workflow: + +```bash +# Infrastructure luôn chạy Docker +cd deployments/local && docker-compose up -d redis traefik + +# Service đang dev chạy native (hot reload nhanh) +pnpm --filter @goodgo/auth-service dev + +# Services khác có thể chạy Docker nếu cần +docker-compose up -d user-service payment-service +``` + +**Lợi ích:** +- ⚡ Hot reload nhanh nhất cho service đang làm +- 💻 Tiết kiệm RAM - không chạy tất cả containers +- 🐛 Debug dễ dàng - breakpoints, logs trực tiếp +- 🎯 Linh hoạt - chọn service nào chạy native + +### 7. Quản Lý Multiple Services + +```bash +# Chạy selective services với pnpm workspace +pnpm --filter "@goodgo/auth-service" --filter "@goodgo/user-service" dev + +# Hoặc dùng pattern +pnpm --filter "./services/{auth,user}-service" dev +``` + +## Environment Variables + +Dự án sử dụng **Hybrid Environment Configuration** với 2 levels: + +### Level 1: Shared Environment (`deployments/local/.env.local`) + +Configs dùng chung cho tất cả services: + +```bash +# JWT Secrets - MUST be same across all services +JWT_SECRET=dev-jwt-secret-change-in-production-min-32-chars +JWT_REFRESH_SECRET=dev-refresh-secret-change-in-production-min-32-chars +JWT_EXPIRES_IN=15m +JWT_REFRESH_EXPIRES_IN=7d + +# Redis (Docker hostname) +REDIS_HOST=redis +REDIS_PORT=6379 + +# Common configs +NODE_ENV=development +LOG_LEVEL=debug +CORS_ORIGIN=http://localhost:3000,http://localhost:3001 + +# Monitoring (optional) +TRACING_ENABLED=false +``` + +**Tạo file:** +```bash +cp deployments/local/env.local.example deployments/local/.env.local +``` + +### Level 2: Service-Specific Environment (`services//.env.local`) + +Configs riêng cho từng service: + +```bash +# services/auth-service/.env.local + +# Database riêng cho service này +DATABASE_URL=postgresql://user:pass@ep-xxx.neon.tech/goodgo_auth_dev?sslmode=require&pgbouncer=true + +# Service configs +PORT=5001 +SERVICE_NAME=auth-service +API_VERSION=v1 + +# Redis override (native dev - Redis in Docker) +REDIS_HOST=localhost + +# Service-specific configs +EMAIL_SERVICE_URL=http://notification-service:5003 +PROMETHEUS_PORT=9090 +``` + +**Tạo file:** +```bash +cp services/auth-service/env.local.example services/auth-service/.env.local +``` + +### Cách Hoạt Động + +Services load env theo thứ tự: +1. **Shared env** (`deployments/local/.env.local`) - JWT, Redis, common configs +2. **Service env** (`.env.local`) - DATABASE_URL, PORT, overrides + +```bash +# Trong package.json +"dev": "dotenv -e ../../deployments/local/.env.local -e .env.local -- tsx watch src/main.ts" +``` + +**Lợi ích:** +- ✅ JWT secrets giống nhau → services verify tokens của nhau +- ✅ Mỗi service có database riêng → microservices pattern +- ✅ Override configs dễ dàng → REDIS_HOST=localhost cho native dev +- ✅ Không duplicate configs → maintain dễ hơn + +### Important Notes + +1. **JWT Secrets**: MUST be identical across all services +2. **Database**: Each service has its own database (e.g., `goodgo_auth_dev`, `goodgo_user_dev`) +3. **Redis Host**: + - `redis` (Docker hostname) in shared env + - `localhost` (override) in service env for native dev +4. **Never commit**: `.env.local` files are gitignored + +## Các Lệnh Hữu Ích + +```bash +# Development +pnpm dev # Chạy tất cả services +pnpm build # Build tất cả +pnpm test # Test tất cả +pnpm lint # Lint tất cả +pnpm format # Format code + +# Cleanup +pnpm clean # Xóa build artifacts +./scripts/utils/cleanup.sh # Cleanup toàn bộ + +# Database +./scripts/db/migrate.sh auth-service dev # Migration +./scripts/db/seed.sh auth-service # Seed data +./scripts/db/backup.sh auth-service # Backup + +# Docker +docker-compose -f deployments/local/docker-compose.yml up -d # Start +docker-compose -f deployments/local/docker-compose.yml down # Stop +docker-compose -f deployments/local/docker-compose.yml logs -f # Logs +docker-compose -f deployments/local/docker-compose.yml restart # Restart +``` + +## Tài Nguyên Thêm + +- [Getting Started](getting-started.md) - Thiết lập ban đầu +- [Development Guide](development.md) - Quy trình development +- [Neon Database Guide](neon-database.md) - Hướng dẫn database +- [Troubleshooting](troubleshooting.md) - Xử lý sự cố diff --git a/docs/vi/guides/neon-database.md b/docs/vi/guides/neon-database.md new file mode 100644 index 00000000..ec39da2e --- /dev/null +++ b/docs/vi/guides/neon-database.md @@ -0,0 +1,215 @@ +# Hướng Dẫn Neon Database + +Project này sử dụng [Neon PostgreSQL](https://neon.tech) cho tất cả môi trường. + +## Tại Sao Chọn Neon? + +- ✅ **Serverless**: Không cần quản lý infrastructure +- ✅ **Branching**: Database riêng cho dev/staging/prod +- ✅ **Auto-scaling**: Tự động xử lý traffic spikes +- ✅ **Point-in-time restore**: Dễ dàng khôi phục từ lỗi +- ✅ **Free tier**: Hoàn hảo cho development +- ✅ **Connection pooling**: Hỗ trợ PgBouncer tích hợp + +## Bắt Đầu Nhanh + +### 1. Tạo Tài Khoản Neon + +1. Đăng ký tại https://neon.tech +2. Tạo project mới: `goodgo-platform` + +### 2. Tạo Branches + +Trong Neon Console, tạo branches: +- `main` (development) - đã tồn tại +- `staging` - tạo từ main +- `production` - tạo từ main + +### 3. Lấy Connection Strings + +Cho mỗi branch, copy connection string: +- Định dạng: `postgresql://user:password@ep-xxx.region.neon.tech/dbname?sslmode=require` +- Thêm `?pgbouncer=true` cho connection pooling (khuyến nghị) + +### 4. Cấu Hình Local Development + +```bash +# Tạo .env.local +cp deployments/local/env.local.example deployments/local/.env.local + +# Chỉnh sửa .env.local và thêm: +DATABASE_URL=postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true +``` + +### 5. Chạy Migrations + +```bash +./scripts/db/migrate.sh auth-service dev +``` + +## Định Dạng Connection String + +``` +postgresql://[user]:[password]@[endpoint]/[dbname]?sslmode=require&pgbouncer=true +``` + +**Tham số**: +- `sslmode=require` - Bắt buộc cho Neon +- `pgbouncer=true` - Bật connection pooling (khuyến nghị) + +## Cấu Hình Môi Trường + +### Local Development + +File: `deployments/local/.env.local` + +```bash +DATABASE_URL=postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true +``` + +### Staging + +Lưu trong GitHub Secrets: `NEON_DATABASE_URL_STAGING` + +Hoặc trong Kubernetes: +```bash +kubectl create secret generic auth-service-secrets \ + --from-literal=database-url='postgresql://...' \ + -n staging +``` + +### Production + +Lưu trong GitHub Secrets: `NEON_DATABASE_URL_PRODUCTION` + +Hoặc trong Kubernetes: +```bash +kubectl create secret generic auth-service-secrets \ + --from-literal=database-url='postgresql://...' \ + -n production +``` + +## Migrations + +### Development + +```bash +# Tạo migration mới +./scripts/db/migrate.sh auth-service dev + +# Điều này sẽ: +# 1. Tạo migration file +# 2. Áp dụng vào database +# 3. Cập nhật Prisma Client +``` + +### Staging/Production + +Migrations chạy tự động trong CI/CD: +- Trước khi deploy lên staging +- Trước khi deploy lên production (cần approval) + +Migration thủ công: +```bash +./scripts/db/migrate.sh auth-service deploy +``` + +## Backup & Restore + +### Backup Tự Động + +Neon cung cấp backup tự động. Truy cập qua Neon Console: +- Point-in-time restore +- Branch restore +- Export data + +### Backup Thủ Công + +```bash +./scripts/db/backup.sh auth-service +``` + +Điều này tạo file SQL dump trong thư mục `backups/`. + +### Restore + +```bash +# Từ Neon Console (khuyến nghị) +# Hoặc sử dụng psql: +psql $DATABASE_URL < backup.sql +``` + +## Monitoring + +Theo dõi databases qua Neon Console: +- Connection metrics +- Query performance +- Storage usage +- Branch status + +## Xử Lý Sự Cố + +### Vấn Đề Kết Nối + +1. **Kiểm tra định dạng connection string** + - Phải bao gồm `?sslmode=require` + - Xác minh credentials + +2. **Kiểm tra IP allowlist** + - Neon có thể giới hạn IPs + - Thêm IP của bạn trong Neon Console + +3. **Kiểm tra branch status** + - Đảm bảo branch đang active + - Kiểm tra maintenance + +### Vấn Đề Migration + +1. **DATABASE_URL chưa set** + ```bash + export DATABASE_URL="your-neon-url" + ``` + +2. **Schema không khớp** + ```bash + # Reset và migrate lại (chỉ dev!) + pnpm prisma migrate reset + ``` + +3. **Connection timeout** + - Thêm `?pgbouncer=true` cho pooling + - Kiểm tra Neon console cho limits + +### Vấn Đề Hiệu Suất + +1. **Bật connection pooling** + - Thêm `?pgbouncer=true` vào connection string + +2. **Kiểm tra query performance** + - Sử dụng Neon Console query analyzer + - Xem lại slow queries + +3. **Tối ưu indexes** + - Xem lại Prisma schema + - Thêm indexes cho các query thường dùng + +## Tối Ưu Chi Phí + +- **Free tier**: 0.5 GB storage, đủ cho dev +- **Staging**: Sử dụng free tier hoặc plan trả phí tối thiểu +- **Production**: Scale dựa trên usage +- **Branching**: Free branches cho testing + +## Best Practices + +1. **Luôn sử dụng connection pooling**: `?pgbouncer=true` +2. **Sử dụng SSL**: `?sslmode=require` +3. **Tách branches**: Một branch cho mỗi môi trường +4. **Backup thường xuyên**: Sử dụng automatic backups của Neon +5. **Theo dõi usage**: Kiểm tra Neon Console thường xuyên + +## Tài Nguyên + +- [Neon Documentation](https://neon.tech/docs) +- [Neon Console](https://console.neon.tech) +- [Prisma + Neon Guide](https://neon.tech/docs/guides/prisma) diff --git a/docs/vi/guides/troubleshooting.md b/docs/vi/guides/troubleshooting.md new file mode 100644 index 00000000..37135837 --- /dev/null +++ b/docs/vi/guides/troubleshooting.md @@ -0,0 +1,57 @@ +# Hướng Dẫn Xử Lý Sự Cố + +## Các Vấn Đề Thường Gặp + +### Kết Nối Database Thất Bại + +**Triệu chứng**: Service không thể kết nối database + +**Giải pháp**: +1. Kiểm tra PostgreSQL có đang chạy: `docker ps` +2. Xác minh DATABASE_URL trong .env +3. Kiểm tra kết nối mạng: `docker network ls` +4. Xem logs: `docker logs postgres-auth-local` + +### Port Đã Được Sử Dụng + +**Triệu chứng**: Service không khởi động với lỗi port + +**Giải pháp**: +1. Tìm process đang dùng port: `lsof -i :5001` +2. Kill process hoặc thay đổi PORT trong .env +3. Kiểm tra docker-compose cho port conflicts + +### Prisma Client Chưa Được Generate + +**Triệu chứng**: Lỗi import cho Prisma Client + +**Giải pháp**: +```bash +cd services/auth-service +pnpm prisma generate +``` + +### Build Failures + +**Triệu chứng**: Lỗi TypeScript hoặc build + +**Giải pháp**: +1. Xóa build artifacts: `./scripts/utils/cleanup.sh` +2. Cài đặt lại dependencies: `pnpm install` +3. Kiểm tra lỗi TypeScript: `pnpm typecheck` + +### Traefik Không Routing + +**Triệu chứng**: Lỗi 404 từ Traefik + +**Giải pháp**: +1. Kiểm tra Traefik dashboard: http://localhost:8080 +2. Xác minh service labels trong docker-compose +3. Kiểm tra cấu hình routes.yml +4. Xem Traefik logs: `docker logs traefik-local` + +## Tìm Kiếm Trợ Giúp + +1. Kiểm tra service logs: `./scripts/dev/logs.sh ` +2. Xem lại GitHub Issues +3. Liên hệ team lead diff --git a/docs/vi/onboarding/new-developer-guide.md b/docs/vi/onboarding/new-developer-guide.md new file mode 100644 index 00000000..4ba64455 --- /dev/null +++ b/docs/vi/onboarding/new-developer-guide.md @@ -0,0 +1,89 @@ +# Hướng Dẫn Cho Developer Mới + +Chào mừng đến với team GoodGo Microservices Platform! + +## Checklist Ngày Đầu Tiên + +- [ ] Quyền truy cập GitHub repository +- [ ] Quyền truy cập môi trường development +- [ ] Docker đã cài đặt và đang chạy +- [ ] Node.js và PNPM đã cài đặt +- [ ] IDE đã cấu hình (khuyến nghị VS Code) +- [ ] Đã đọc hướng dẫn này + +## Thiết Lập Môi Trường Development + +1. **Clone repository** + ```bash + git clone + cd Base + ``` + +2. **Chạy script khởi tạo** + ```bash + ./scripts/setup/init-project.sh + ``` + +3. **Khởi động infrastructure local** + ```bash + cd deployments/local + docker-compose up -d + ``` + +4. **Xác minh setup** + - Kiểm tra Traefik: http://localhost:8080 + - Kiểm tra API: http://localhost/api/v1/health + +## Công Cụ Development + +### VS Code Extensions Khuyến Nghị + +- ESLint +- Prettier +- Prisma +- Docker +- GitLens + +### Các Lệnh Hữu Ích + +```bash +# Khởi động tất cả services +./scripts/dev/start-all.sh + +# Khởi động service cụ thể +./scripts/dev/start-service.sh auth-service + +# Xem logs +./scripts/dev/logs.sh auth-service + +# Chạy migrations +./scripts/db/migrate.sh auth-service dev + +# Chạy tests +pnpm test +``` + +## Tiêu Chuẩn Code + +- **TypeScript**: Bật strict mode +- **Linting**: ESLint với shared config +- **Formatting**: Prettier +- **Commits**: Định dạng Conventional Commits +- **Tests**: Tối thiểu 80% coverage + +## Tìm Kiếm Trợ Giúp + +- Xem [Tài Liệu](../guides/) +- Hỏi trong Slack channel của team +- Xem lại các ví dụ code hiện có +- Pair với senior developer + +## Các Bước Tiếp Theo + +1. Chọn một task nhỏ từ backlog +2. Tạo feature branch +3. Implement và test +4. Tạo pull request +5. Nhận code review + +Chúc may mắn! 🚀 diff --git a/docs/vi/runbooks/incident-response.md b/docs/vi/runbooks/incident-response.md new file mode 100644 index 00000000..81e272ba --- /dev/null +++ b/docs/vi/runbooks/incident-response.md @@ -0,0 +1,65 @@ +# Runbook Phản Ứng Sự Cố + +## Mức Độ Nghiêm Trọng + +- **P0 - Nghiêm Trọng**: Dịch vụ hoàn toàn ngừng hoạt động, mất dữ liệu +- **P1 - Cao**: Chức năng chính bị lỗi, ảnh hưởng đến nhiều người dùng +- **P2 - Trung Bình**: Chức năng phụ bị lỗi, có giải pháp thay thế +- **P3 - Thấp**: Lỗi giao diện, không ảnh hưởng người dùng + +## Quy Trình Phản Ứng + +### 1. Xác Nhận Sự Cố + +- Xác định mức độ nghiêm trọng +- Thông báo cho team qua Slack/email +- Tạo ticket sự cố + +### 2. Điều Tra + +- Kiểm tra các endpoint health của service +- Xem logs: `./scripts/dev/logs.sh ` +- Kiểm tra monitoring dashboards (Grafana) +- Xem lại các deployment gần đây + +### 3. Giảm Thiểu + +- Áp dụng các sửa lỗi nhanh nếu có +- Rollback nếu deployment gần đây gây ra vấn đề +- Scale up nếu thiếu tài nguyên + +### 4. Giải Quyết + +- Triển khai sửa lỗi vĩnh viễn +- Xác minh giải pháp +- Cập nhật tài liệu + +### 5. Hậu Phân Tích + +- Ghi lại sự cố +- Xác định nguyên nhân gốc rễ +- Tạo các hành động cần thực hiện +- Cập nhật runbooks + +## Các Tình Huống Thường Gặp + +### Service Ngừng Hoạt Động + +1. Kiểm tra Kubernetes pods: `kubectl get pods -n ` +2. Kiểm tra pod logs: `kubectl logs -n ` +3. Khởi động lại service: `kubectl rollout restart deployment/ -n ` +4. Nếu vẫn lỗi, rollback: `kubectl rollout undo deployment/ -n ` + +### Vấn Đề Database + +1. Kiểm tra kết nối database +2. Xem lại các query chậm +3. Kiểm tra connection pool +4. Scale database nếu cần + +### Tỷ Lệ Lỗi Cao + +1. Kiểm tra error logs +2. Xem lại các thay đổi gần đây +3. Kiểm tra các dependencies bên ngoài +4. Triển khai circuit breaker nếu cần diff --git a/docs/vi/runbooks/rollback-procedure.md b/docs/vi/runbooks/rollback-procedure.md new file mode 100644 index 00000000..a5862c65 --- /dev/null +++ b/docs/vi/runbooks/rollback-procedure.md @@ -0,0 +1,71 @@ +# Quy Trình Rollback + +## Khi Nào Cần Rollback + +- Service ngừng hoạt động hoặc không ổn định +- Lỗi nghiêm trọng được phát hiện +- Hiệu suất suy giảm +- Rủi ro hỏng dữ liệu + +## Các Bước Rollback + +### Rollback Kubernetes + +1. **Xác định phiên bản hiện tại** + ```bash + kubectl get deployment auth-service -n production -o jsonpath='{.spec.template.spec.containers[0].image}' + ``` + +2. **Rollback về phiên bản trước** + ```bash + kubectl rollout undo deployment/auth-service -n production + ``` + +3. **Xác minh rollback** + ```bash + kubectl rollout status deployment/auth-service -n production + ``` + +4. **Kiểm tra health của service** + ```bash + curl https://api.goodgo.vn/health + ``` + +### Rollback Database Migration + +**Lưu ý**: Prisma không hỗ trợ rollback tự động. Tạo migration mới để đảo ngược thay đổi. + +1. Tạo migration đảo ngược: + ```bash + cd services/auth-service + pnpm prisma migrate dev --name rollback_previous_change + ``` + +2. Áp dụng migration đảo ngược: + ```bash + pnpm prisma migrate deploy + ``` + +### Rollback Docker Compose + +1. Dừng các container hiện tại: + ```bash + docker-compose down + ``` + +2. Checkout phiên bản trước: + ```bash + git checkout + ``` + +3. Rebuild và khởi động: + ```bash + docker-compose up -d --build + ``` + +## Sau Khi Rollback + +1. Xác minh chức năng +2. Theo dõi metrics +3. Ghi lại lý do rollback +4. Lập kế hoạch sửa lỗi cho deployment tiếp theo diff --git a/infra/databases/docker-compose.databases.yml b/infra/databases/docker-compose.databases.yml new file mode 100644 index 00000000..b3ca2d60 --- /dev/null +++ b/infra/databases/docker-compose.databases.yml @@ -0,0 +1,48 @@ +version: '3.8' + +services: + postgres: + image: postgres:16-alpine + container_name: postgres-auth + environment: + POSTGRES_USER: ${POSTGRES_USER:-devuser} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-devpass} + POSTGRES_DB: ${POSTGRES_DB:-auth_db} + ports: + - "${POSTGRES_PORT:-5432}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql + - ./postgres/postgres.conf:/etc/postgresql/postgresql.conf + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-devuser}"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - microservices-network + + redis: + image: redis:7-alpine + container_name: redis-cache + command: redis-server /etc/redis/redis.conf + ports: + - "${REDIS_PORT:-6379}:6379" + volumes: + - redis_data:/data + - ./redis/redis.conf:/etc/redis/redis.conf + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + networks: + - microservices-network + +volumes: + postgres_data: + redis_data: + +networks: + microservices-network: + external: true diff --git a/infra/databases/neon/README.md b/infra/databases/neon/README.md new file mode 100644 index 00000000..9e3432d2 --- /dev/null +++ b/infra/databases/neon/README.md @@ -0,0 +1,125 @@ +# Neon Database Setup + +This project uses [Neon PostgreSQL](https://neon.tech) for all environments (dev, staging, production). + +## Why Neon? + +- ✅ Serverless PostgreSQL with auto-scaling +- ✅ Branching support (dev, staging, production branches) +- ✅ Point-in-time restore +- ✅ Free tier for development +- ✅ Built-in connection pooling +- ✅ No infrastructure management needed + +## Setup Instructions + +### 1. Create Neon Project + +1. Sign up at https://neon.tech +2. Create a new project: `goodgo-platform` +3. Note your connection string from the main branch + +### 2. Create Branches + +Create branches for each environment: + +```bash +# Using Neon Console or CLI +# Main branch = development +# Create: staging branch +# Create: production branch +``` + +### 3. Get Connection Strings + +For each branch, get the connection string: +- Format: `postgresql://user:password@ep-xxx.region.neon.tech/dbname?sslmode=require` +- Add `?pgbouncer=true` for connection pooling (recommended) + +### 4. Configure Environment Variables + +#### Local Development + +Create `deployments/local/.env.local`: + +```bash +DATABASE_URL=postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true +``` + +#### Staging/Production + +Store in GitHub Secrets: +- `NEON_DATABASE_URL_STAGING` +- `NEON_DATABASE_URL_PRODUCTION` + +Or in Kubernetes Secrets (see `deployments/staging/kubernetes/secrets.yaml.example`) + +## Connection Pooling + +Neon provides built-in connection pooling. Add `?pgbouncer=true` to your connection string: + +``` +postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true +``` + +## Migration + +### Development + +```bash +cd services/auth-service +pnpm prisma migrate dev +``` + +### Staging/Production + +Migrations run automatically in CI/CD workflows before deployment. + +## Backup & Restore + +Neon provides automatic backups and point-in-time restore via their console. + +For manual backup: + +```bash +# Using pg_dump +pg_dump $DATABASE_URL > backup.sql + +# Restore +psql $DATABASE_URL < backup.sql +``` + +## Monitoring + +Monitor your databases via Neon Console: +- Connection metrics +- Query performance +- Storage usage +- Branch status + +## Cost Optimization + +- **Free tier**: Sufficient for development +- **Staging**: Use free tier or minimal paid plan +- **Production**: Scale based on usage + +## Troubleshooting + +### Connection Issues + +1. Check connection string format +2. Verify SSL mode: `?sslmode=require` +3. Check IP allowlist in Neon console (if enabled) +4. Verify credentials + +### Migration Issues + +1. Ensure DATABASE_URL is set correctly +2. Check Prisma schema matches database +3. Review migration files + +### Performance Issues + +1. Enable connection pooling: `?pgbouncer=true` +2. Check query performance in Neon console +3. Review indexes in Prisma schema diff --git a/infra/databases/neon/setup.sh b/infra/databases/neon/setup.sh new file mode 100755 index 00000000..d326d18a --- /dev/null +++ b/infra/databases/neon/setup.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +set -e + +echo "🚀 Neon Database Setup Script" +echo "" + +# Check if Neon CLI is installed +if ! command -v neonctl &> /dev/null; then + echo "⚠️ Neon CLI not found. Installing..." + echo "Visit: https://neon.tech/docs/cli/install" + echo "" + read -p "Do you want to continue with manual setup? (y/n): " continue + if [ "$continue" != "y" ]; then + exit 1 + fi +fi + +echo "This script will help you set up Neon database branches." +echo "" +echo "Prerequisites:" +echo "1. Neon account created at https://neon.tech" +echo "2. Neon project created" +echo "3. Neon CLI installed (optional)" +echo "" + +read -p "Enter your Neon project name (default: goodgo-platform): " project_name +project_name=${project_name:-goodgo-platform} + +echo "" +echo "📋 Setup Steps:" +echo "" +echo "1. Go to https://console.neon.tech" +echo "2. Select your project: $project_name" +echo "3. Create branches:" +echo " - staging (from main)" +echo " - production (from main)" +echo "" +echo "4. Get connection strings for each branch:" +echo " - Development (main branch)" +echo " - Staging branch" +echo " - Production branch" +echo "" +echo "5. Update environment files:" +echo " - deployments/local/.env.local" +echo " - GitHub Secrets (for CI/CD)" +echo " - Kubernetes Secrets (for deployments)" +echo "" + +read -p "Enter development DATABASE_URL (or press Enter to skip): " dev_url +if [ -n "$dev_url" ]; then + echo "DATABASE_URL=$dev_url" >> deployments/local/.env.local + echo "✅ Added to deployments/local/.env.local" +fi + +echo "" +echo "📝 Next steps:" +echo "" +echo "1. Add staging URL to GitHub Secrets: NEON_DATABASE_URL_STAGING" +echo "2. Add production URL to GitHub Secrets: NEON_DATABASE_URL_PRODUCTION" +echo "3. Create Kubernetes secrets for staging/production" +echo "4. Run migrations: ./scripts/db/migrate.sh auth-service dev" +echo "" +echo "✅ Setup complete! See infra/databases/neon/README.md for details." diff --git a/infra/databases/postgres/init.sql b/infra/databases/postgres/init.sql new file mode 100644 index 00000000..f03bee41 --- /dev/null +++ b/infra/databases/postgres/init.sql @@ -0,0 +1,8 @@ +-- Initial database setup script +-- This script runs when PostgreSQL container starts for the first time + +-- Create extensions if needed +-- CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Note: Prisma migrations handle schema creation +-- This file is for any additional setup needed diff --git a/infra/databases/postgres/postgres.conf b/infra/databases/postgres/postgres.conf new file mode 100644 index 00000000..dfcc555c --- /dev/null +++ b/infra/databases/postgres/postgres.conf @@ -0,0 +1,15 @@ +# PostgreSQL configuration +# Customize as needed for your environment + +max_connections = 200 +shared_buffers = 256MB +effective_cache_size = 1GB +maintenance_work_mem = 64MB +checkpoint_completion_target = 0.9 +wal_buffers = 16MB +default_statistics_target = 100 +random_page_cost = 1.1 +effective_io_concurrency = 200 +work_mem = 4MB +min_wal_size = 1GB +max_wal_size = 4GB diff --git a/infra/databases/redis/redis.conf b/infra/databases/redis/redis.conf new file mode 100644 index 00000000..e4e2c01a --- /dev/null +++ b/infra/databases/redis/redis.conf @@ -0,0 +1,21 @@ +# Redis configuration + +# Network +bind 0.0.0.0 +protected-mode yes +port 6379 + +# Memory +maxmemory 256mb +maxmemory-policy allkeys-lru + +# Persistence +save 900 1 +save 300 10 +save 60 10000 + +# Logging +loglevel notice + +# Security +# requirepass your-redis-password-here diff --git a/infra/docker/docker-compose.dev.yml b/infra/docker/docker-compose.dev.yml new file mode 100644 index 00000000..d4470694 --- /dev/null +++ b/infra/docker/docker-compose.dev.yml @@ -0,0 +1,9 @@ +version: '3.8' + +networks: + microservices-network: + driver: bridge + +volumes: + postgres_data: + redis_data: diff --git a/infra/docker/docker-compose.prod.yml b/infra/docker/docker-compose.prod.yml new file mode 100644 index 00000000..2566d32d --- /dev/null +++ b/infra/docker/docker-compose.prod.yml @@ -0,0 +1,12 @@ +version: '3.8' + +networks: + microservices-network: + driver: bridge + +volumes: + postgres_data: + redis_data: + prometheus_data: + grafana_data: + loki_data: diff --git a/infra/observability/docker-compose.observability.yml b/infra/observability/docker-compose.observability.yml new file mode 100644 index 00000000..03ec5fa9 --- /dev/null +++ b/infra/observability/docker-compose.observability.yml @@ -0,0 +1,58 @@ +version: '3.8' + +services: + prometheus: + image: prom/prometheus:latest + container_name: prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.console.libraries=/etc/prometheus/console_libraries' + - '--web.console.templates=/etc/prometheus/consoles' + ports: + - "9090:9090" + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + - ./prometheus/rules:/etc/prometheus/rules + - prometheus_data:/prometheus + networks: + - microservices-network + + grafana: + image: grafana/grafana:latest + container_name: grafana + ports: + - "3001:3000" + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/datasources:/etc/grafana/provisioning/datasources + - ./grafana/dashboards:/etc/grafana/provisioning/dashboards + networks: + - microservices-network + depends_on: + - prometheus + + loki: + image: grafana/loki:latest + container_name: loki + ports: + - "3100:3100" + command: -config.file=/etc/loki/loki-config.yml + volumes: + - ./loki/loki-config.yml:/etc/loki/loki-config.yml + - loki_data:/loki + networks: + - microservices-network + +volumes: + prometheus_data: + grafana_data: + loki_data: + +networks: + microservices-network: + external: true diff --git a/infra/observability/grafana/dashboards/service-overview.json b/infra/observability/grafana/dashboards/service-overview.json new file mode 100644 index 00000000..03325545 --- /dev/null +++ b/infra/observability/grafana/dashboards/service-overview.json @@ -0,0 +1,39 @@ +{ + "dashboard": { + "title": "Service Overview", + "tags": ["microservices"], + "timezone": "browser", + "panels": [ + { + "title": "Request Rate", + "type": "graph", + "targets": [ + { + "expr": "rate(http_requests_total[5m])", + "legendFormat": "{{service}}" + } + ] + }, + { + "title": "Error Rate", + "type": "graph", + "targets": [ + { + "expr": "rate(http_requests_total{status=~\"5..\"}[5m])", + "legendFormat": "{{service}}" + } + ] + }, + { + "title": "Response Time (p95)", + "type": "graph", + "targets": [ + { + "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))", + "legendFormat": "{{service}}" + } + ] + } + ] + } +} diff --git a/infra/observability/grafana/datasources/prometheus.yml b/infra/observability/grafana/datasources/prometheus.yml new file mode 100644 index 00000000..f4f70022 --- /dev/null +++ b/infra/observability/grafana/datasources/prometheus.yml @@ -0,0 +1,15 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: true + + - name: Loki + type: loki + access: proxy + url: http://loki:3100 + editable: true diff --git a/infra/observability/loki/loki-config.yml b/infra/observability/loki/loki-config.yml new file mode 100644 index 00000000..5f484082 --- /dev/null +++ b/infra/observability/loki/loki-config.yml @@ -0,0 +1,41 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + grpc_listen_port: 9096 + +common: + path_prefix: /loki + storage: + filesystem: + chunks_directory: /loki/chunks + rules_directory: /loki/rules + replication_factor: 1 + ring: + instance_addr: 127.0.0.1 + kvstore: + store: inmemory + +schema_config: + configs: + - from: 2020-10-24 + store: boltdb-shipper + object_store: filesystem + schema: v11 + index: + prefix: index_ + period: 24h + +ruler: + alertmanager_url: http://localhost:9093 + +limits_config: + reject_old_samples: true + reject_old_samples_max_age: 168h + +chunk_store_config: + max_look_back_period: 0s + +table_manager: + retention_deletes_enabled: false + retention_period: 0s diff --git a/infra/observability/prometheus/prometheus.yml b/infra/observability/prometheus/prometheus.yml new file mode 100644 index 00000000..8ed0ae41 --- /dev/null +++ b/infra/observability/prometheus/prometheus.yml @@ -0,0 +1,29 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + external_labels: + cluster: 'microservices' + environment: 'development' + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + - job_name: 'auth-service' + static_configs: + - targets: ['auth-service:9090'] + metrics_path: '/metrics' + + - job_name: 'traefik' + static_configs: + - targets: ['traefik:8080'] + metrics_path: '/metrics' + +rule_files: + - 'rules/*.yml' + +alerting: + alertmanagers: + - static_configs: + - targets: [] diff --git a/infra/observability/prometheus/rules/service-alerts.yml b/infra/observability/prometheus/rules/service-alerts.yml new file mode 100644 index 00000000..7b6e7f8b --- /dev/null +++ b/infra/observability/prometheus/rules/service-alerts.yml @@ -0,0 +1,30 @@ +groups: + - name: service_alerts + interval: 30s + rules: + - alert: HighErrorRate + expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05 + for: 5m + labels: + severity: critical + annotations: + summary: "High error rate detected" + description: "Service {{ $labels.service }} has error rate above 5%" + + - alert: ServiceDown + expr: up == 0 + for: 1m + labels: + severity: critical + annotations: + summary: "Service is down" + description: "Service {{ $labels.job }} is down" + + - alert: HighResponseTime + expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1 + for: 5m + labels: + severity: warning + annotations: + summary: "High response time" + description: "95th percentile response time is above 1s" diff --git a/infra/secrets/.gitignore b/infra/secrets/.gitignore new file mode 100644 index 00000000..794096a9 --- /dev/null +++ b/infra/secrets/.gitignore @@ -0,0 +1,4 @@ +# Ignore all secrets +* +!.gitignore +!.env.example diff --git a/infra/traefik/README.md b/infra/traefik/README.md new file mode 100644 index 00000000..97fdb1c9 --- /dev/null +++ b/infra/traefik/README.md @@ -0,0 +1,25 @@ +# Traefik Configuration + +Traefik reverse proxy and load balancer configuration. + +## Files + +- `traefik.yml` - Static configuration +- `dynamic/routes.yml` - HTTP routing rules +- `dynamic/middlewares.yml` - Middleware definitions (CORS, rate limiting, security headers) + +## Usage + +Traefik will automatically discover services via Docker labels or file-based configuration. + +## Routes + +- `/api/v1/auth/*` -> Auth Service +- `/api/v1/users/*` -> Auth Service + +## Middlewares + +- `secure-headers` - Security headers +- `cors` - CORS configuration +- `auth-ratelimit` - Rate limiting for auth endpoints +- `compress` - Response compression diff --git a/infra/traefik/dynamic/middlewares.yml b/infra/traefik/dynamic/middlewares.yml new file mode 100644 index 00000000..b253d595 --- /dev/null +++ b/infra/traefik/dynamic/middlewares.yml @@ -0,0 +1,38 @@ +http: + middlewares: + secure-headers: + headers: + sslRedirect: true + stsSeconds: 31536000 + contentTypeNosniff: true + browserXssFilter: true + frameDeny: true + customRequestHeaders: + X-Forwarded-Proto: "https" + + cors: + headers: + accessControlAllowMethods: + - GET + - POST + - PUT + - DELETE + - PATCH + - OPTIONS + accessControlAllowOriginList: + - "http://localhost:3000" + - "http://localhost:3001" + - "https://goodgo.vn" + accessControlAllowHeaders: + - Content-Type + - Authorization + accessControlAllowCredentials: true + accessControlMaxAge: 86400 + + auth-ratelimit: + rateLimit: + average: 100 + burst: 50 + + compress: + compress: {} diff --git a/infra/traefik/dynamic/routes.yml b/infra/traefik/dynamic/routes.yml new file mode 100644 index 00000000..ede3fd78 --- /dev/null +++ b/infra/traefik/dynamic/routes.yml @@ -0,0 +1,55 @@ +http: + routers: + auth-service-router: + rule: "Host(`api.goodgo.vn`) && PathPrefix(`/api/v1/auth`)" + service: auth-service + middlewares: + - auth-ratelimit + - cors + - secure-headers + entryPoints: + - web + + auth-service-users-router: + rule: "Host(`api.goodgo.vn`) && PathPrefix(`/api/v1/users`)" + service: auth-service + middlewares: + - auth-ratelimit + - cors + - secure-headers + entryPoints: + - web + + web-admin-router: + rule: "Host(`admin.goodgo.vn`) || Host(`admin.localhost`)" + service: web-admin + middlewares: + - cors + - secure-headers + entryPoints: + - web + + web-client-router: + rule: "Host(`goodgo.vn`) || Host(`localhost`)" + service: web-client + middlewares: + - cors + - secure-headers + entryPoints: + - web + + services: + auth-service: + loadBalancer: + servers: + - url: "http://auth-service:5001" + + web-admin: + loadBalancer: + servers: + - url: "http://web-admin:3000" + + web-client: + loadBalancer: + servers: + - url: "http://web-client:3001" \ No newline at end of file diff --git a/infra/traefik/traefik.yml b/infra/traefik/traefik.yml new file mode 100644 index 00000000..52226467 --- /dev/null +++ b/infra/traefik/traefik.yml @@ -0,0 +1,26 @@ +api: + dashboard: true + insecure: true + +entryPoints: + web: + address: ":80" + websecure: + address: ":443" + +providers: + docker: + endpoint: "unix:///var/run/docker.sock" + exposedByDefault: false + file: + directory: "/etc/traefik/dynamic" + watch: true + +log: + level: INFO + +accessLog: {} + +metrics: + prometheus: + entryPoint: web diff --git a/package.json b/package.json new file mode 100644 index 00000000..a4df2522 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "@goodgo/monorepo", + "version": "1.0.0", + "private": true, + "description": "Enterprise-grade microservices monorepo with Auth Service, Web & Mobile apps", + "workspaces": [ + "apps/*", + "services/*", + "packages/*" + ], + "scripts": { + "dev": "pnpm --parallel -r dev", + "build": "pnpm -r build", + "test": "pnpm -r test", + "lint": "pnpm -r lint", + "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"", + "clean": "pnpm -r clean && rm -rf node_modules", + "typecheck": "pnpm -r typecheck" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "typescript": "^5.3.3", + "prettier": "^3.2.4", + "eslint": "^8.56.0", + "turbo": "^1.11.2" + }, + "engines": { + "node": ">=20.0.0", + "pnpm": ">=8.0.0" + }, + "packageManager": "pnpm@8.15.0" +} diff --git a/packages/auth-sdk/README.md b/packages/auth-sdk/README.md new file mode 100644 index 00000000..c2e4c2f0 --- /dev/null +++ b/packages/auth-sdk/README.md @@ -0,0 +1,22 @@ +# @goodgo/auth-sdk + +Authentication SDK utilities for JWT token management. + +## Usage + +```typescript +import { verifyToken, createToken, isTokenExpired } from '@goodgo/auth-sdk'; + +// Create token +const token = createToken( + { userId: '123', email: 'user@example.com', role: 'USER' }, + 'secret', + '15m' +); + +// Verify token +const payload = verifyToken(token, { secret: 'secret' }); + +// Check expiration +const expired = isTokenExpired(token); +``` diff --git a/packages/auth-sdk/package.json b/packages/auth-sdk/package.json new file mode 100644 index 00000000..d2853d5a --- /dev/null +++ b/packages/auth-sdk/package.json @@ -0,0 +1,22 @@ +{ + "name": "@goodgo/auth-sdk", + "version": "1.0.0", + "description": "Authentication SDK utilities", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist" + }, + "dependencies": { + "@goodgo/types": "workspace:*", + "jsonwebtoken": "^9.0.2" + }, + "devDependencies": { + "@goodgo/tsconfig": "workspace:*", + "@types/jsonwebtoken": "^9.0.5", + "@types/node": "^20.11.0", + "typescript": "^5.3.3" + } +} diff --git a/packages/auth-sdk/src/index.ts b/packages/auth-sdk/src/index.ts new file mode 100644 index 00000000..5c0c404d --- /dev/null +++ b/packages/auth-sdk/src/index.ts @@ -0,0 +1,68 @@ +import * as jwt from 'jsonwebtoken'; +import type { SignOptions } from 'jsonwebtoken'; +import { TokenPayload } from '@goodgo/types'; + +export interface VerifyTokenOptions { + secret: string; + ignoreExpiration?: boolean; +} + +export const verifyToken = (token: string, options: VerifyTokenOptions): TokenPayload => { + try { + const decoded = jwt.verify(token, options.secret, { + ignoreExpiration: options.ignoreExpiration || false, + }) as TokenPayload; + return decoded; + } catch (error) { + throw new Error('Invalid or expired token'); + } +}; + +export const decodeToken = (token: string): TokenPayload | null => { + try { + return jwt.decode(token) as TokenPayload; + } catch (error) { + return null; + } +}; + +export const createToken = ( + payload: Omit, + secret: string, + expiresIn: string = '15m' +): string => { + return jwt.sign(payload, secret, { expiresIn } as SignOptions); +}; + +export const isTokenExpired = (token: string): boolean => { + try { + const decoded = decodeToken(token); + if (!decoded || !decoded.exp) { + return true; + } + return decoded.exp * 1000 < Date.now(); + } catch { + return true; + } +}; + +export const extractTokenFromHeader = (authHeader: string | undefined): string | null => { + if (!authHeader) { + return null; + } + + const parts = authHeader.split(' '); + if (parts.length !== 2 || parts[0] !== 'Bearer') { + return null; + } + + return parts[1]; +}; + +export default { + verifyToken, + decodeToken, + createToken, + isTokenExpired, + extractTokenFromHeader, +}; diff --git a/packages/auth-sdk/tsconfig.json b/packages/auth-sdk/tsconfig.json new file mode 100644 index 00000000..6c2ca3d9 --- /dev/null +++ b/packages/auth-sdk/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@goodgo/tsconfig/node.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/config/eslint-config/index.js b/packages/config/eslint-config/index.js new file mode 100644 index 00000000..a97e7f91 --- /dev/null +++ b/packages/config/eslint-config/index.js @@ -0,0 +1,37 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + project: './tsconfig.json', + }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + 'prettier', + ], + plugins: ['@typescript-eslint', 'import'], + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + 'import/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], + 'newlines-between': 'always', + alphabetize: { order: 'asc' }, + }, + ], + }, + settings: { + 'import/resolver': { + typescript: { + alwaysTryTypes: true, + }, + }, + }, +}; diff --git a/packages/config/eslint-config/package.json b/packages/config/eslint-config/package.json new file mode 100644 index 00000000..e344a214 --- /dev/null +++ b/packages/config/eslint-config/package.json @@ -0,0 +1,13 @@ +{ + "name": "@goodgo/eslint-config", + "version": "1.0.0", + "description": "Shared ESLint configuration", + "main": "index.js", + "dependencies": { + "@typescript-eslint/eslint-plugin": "^6.19.0", + "@typescript-eslint/parser": "^6.19.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1" + } +} diff --git a/packages/config/prettier-config/index.js b/packages/config/prettier-config/index.js new file mode 100644 index 00000000..8b655268 --- /dev/null +++ b/packages/config/prettier-config/index.js @@ -0,0 +1,10 @@ +module.exports = { + semi: true, + trailingComma: 'es5', + singleQuote: true, + printWidth: 100, + tabWidth: 2, + useTabs: false, + arrowParens: 'always', + endOfLine: 'lf', +}; diff --git a/packages/config/prettier-config/package.json b/packages/config/prettier-config/package.json new file mode 100644 index 00000000..5003d346 --- /dev/null +++ b/packages/config/prettier-config/package.json @@ -0,0 +1,9 @@ +{ + "name": "@goodgo/prettier-config", + "version": "1.0.0", + "description": "Shared Prettier configuration", + "main": "index.js", + "dependencies": { + "prettier": "^3.2.4" + } +} diff --git a/packages/config/tsconfig/base.json b/packages/config/tsconfig/base.json new file mode 100644 index 00000000..24937546 --- /dev/null +++ b/packages/config/tsconfig/base.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + } +} diff --git a/packages/config/tsconfig/nextjs.json b/packages/config/tsconfig/nextjs.json new file mode 100644 index 00000000..869dae45 --- /dev/null +++ b/packages/config/tsconfig/nextjs.json @@ -0,0 +1,23 @@ +{ + "extends": "./base.json", + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "esnext", + "jsx": "preserve", + "moduleResolution": "bundler", + "allowJs": true, + "noEmit": true, + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/config/tsconfig/node.json b/packages/config/tsconfig/node.json new file mode 100644 index 00000000..7ae28a01 --- /dev/null +++ b/packages/config/tsconfig/node.json @@ -0,0 +1,9 @@ +{ + "extends": "./base.json", + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "lib": ["ES2020", "DOM"], + "types": ["node"] + } +} diff --git a/packages/config/tsconfig/package.json b/packages/config/tsconfig/package.json new file mode 100644 index 00000000..e87e4cbd --- /dev/null +++ b/packages/config/tsconfig/package.json @@ -0,0 +1,7 @@ +{ + "name": "@goodgo/tsconfig", + "version": "1.0.0", + "description": "Shared TypeScript configurations", + "main": "base.json", + "files": ["base.json", "node.json", "nextjs.json"] +} diff --git a/packages/http-client/README.md b/packages/http-client/README.md new file mode 100644 index 00000000..6408fbef --- /dev/null +++ b/packages/http-client/README.md @@ -0,0 +1,26 @@ +# @goodgo/http-client + +HTTP client wrapper with Axios, automatic token management, and error handling. + +## Usage + +```typescript +import { createHttpClient } from '@goodgo/http-client'; + +const client = createHttpClient({ + baseURL: 'http://api.example.com', + timeout: 30000, +}); + +// GET request +const response = await client.get('/users'); + +// POST request +const result = await client.post('/auth/login', { + email: 'user@example.com', + password: 'password', +}); + +// Set auth token +client.setAuthToken('your-token-here'); +``` diff --git a/packages/http-client/package.json b/packages/http-client/package.json new file mode 100644 index 00000000..6fd347c8 --- /dev/null +++ b/packages/http-client/package.json @@ -0,0 +1,21 @@ +{ + "name": "@goodgo/http-client", + "version": "1.0.0", + "description": "HTTP client wrapper with Axios", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist" + }, + "dependencies": { + "@goodgo/types": "workspace:*", + "axios": "^1.6.5" + }, + "devDependencies": { + "@goodgo/tsconfig": "workspace:*", + "@types/node": "^20.11.0", + "typescript": "^5.3.3" + } +} diff --git a/packages/http-client/src/index.ts b/packages/http-client/src/index.ts new file mode 100644 index 00000000..0e4df474 --- /dev/null +++ b/packages/http-client/src/index.ts @@ -0,0 +1,110 @@ +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'; +import { ApiResponse } from '@goodgo/types'; + +export interface HttpClientConfig extends AxiosRequestConfig { + baseURL: string; + timeout?: number; + headers?: Record; +} + +export class HttpClient { + private client: AxiosInstance; + + constructor(config: HttpClientConfig) { + this.client = axios.create({ + baseURL: config.baseURL, + timeout: config.timeout || 30000, + headers: { + 'Content-Type': 'application/json', + ...config.headers, + }, + }); + + this.setupInterceptors(); + } + + private setupInterceptors() { + // Request interceptor + this.client.interceptors.request.use( + (config) => { + // Add auth token if available + const token = this.getAuthToken(); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => Promise.reject(error) + ); + + // Response interceptor + this.client.interceptors.response.use( + (response: AxiosResponse) => { + return response; + }, + (error: AxiosError) => { + // Handle common errors + if (error.response?.status === 401) { + // Handle unauthorized - clear token, redirect to login + this.clearAuthToken(); + } + return Promise.reject(error); + } + ); + } + + private getAuthToken(): string | null { + if (typeof window !== 'undefined') { + return localStorage.getItem('accessToken'); + } + return null; + } + + private clearAuthToken(): void { + if (typeof window !== 'undefined') { + localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); + } + } + + async get(url: string, config?: AxiosRequestConfig): Promise> { + const response = await this.client.get>(url, config); + return response.data; + } + + async post(url: string, data?: any, config?: AxiosRequestConfig): Promise> { + const response = await this.client.post>(url, data, config); + return response.data; + } + + async put(url: string, data?: any, config?: AxiosRequestConfig): Promise> { + const response = await this.client.put>(url, data, config); + return response.data; + } + + async patch(url: string, data?: any, config?: AxiosRequestConfig): Promise> { + const response = await this.client.patch>(url, data, config); + return response.data; + } + + async delete(url: string, config?: AxiosRequestConfig): Promise> { + const response = await this.client.delete>(url, config); + return response.data; + } + + setAuthToken(token: string): void { + if (typeof window !== 'undefined') { + localStorage.setItem('accessToken', token); + } + } + + removeAuthToken(): void { + this.clearAuthToken(); + } +} + +export const createHttpClient = (config: HttpClientConfig): HttpClient => { + return new HttpClient(config); +}; + +export default HttpClient; diff --git a/packages/http-client/tsconfig.json b/packages/http-client/tsconfig.json new file mode 100644 index 00000000..6c2ca3d9 --- /dev/null +++ b/packages/http-client/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@goodgo/tsconfig/node.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/logger/README.md b/packages/logger/README.md new file mode 100644 index 00000000..b953b90a --- /dev/null +++ b/packages/logger/README.md @@ -0,0 +1,29 @@ +# @goodgo/logger + +Centralized logging utility using Winston. + +## Usage + +```typescript +import { logger, createLogger } from '@goodgo/logger'; + +// Use default logger +logger.info('Application started'); +logger.error('Error occurred', { error: err }); + +// Create custom logger +const customLogger = createLogger({ + level: 'debug', + serviceName: 'auth-service', + enableFile: true, + logDir: './logs', +}); +``` + +## Configuration + +- `level`: Log level (error, warn, info, debug) +- `serviceName`: Service identifier +- `enableConsole`: Enable console output +- `enableFile`: Enable file logging +- `logDir`: Directory for log files diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 00000000..b04011fc --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,20 @@ +{ + "name": "@goodgo/logger", + "version": "1.0.0", + "description": "Centralized logging utility using Winston", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist" + }, + "dependencies": { + "winston": "^3.11.0" + }, + "devDependencies": { + "@goodgo/tsconfig": "workspace:*", + "@types/node": "^20.11.0", + "typescript": "^5.3.3" + } +} diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts new file mode 100644 index 00000000..a8313f34 --- /dev/null +++ b/packages/logger/src/index.ts @@ -0,0 +1,71 @@ +import winston from 'winston'; + +export interface LoggerConfig { + level?: string; + serviceName?: string; + enableConsole?: boolean; + enableFile?: boolean; + logDir?: string; +} + +const defaultConfig: Required = { + level: process.env.LOG_LEVEL || 'info', + serviceName: process.env.SERVICE_NAME || 'microservice', + enableConsole: true, + enableFile: false, + logDir: './logs', +}; + +export const createLogger = (config: LoggerConfig = {}) => { + const finalConfig = { ...defaultConfig, ...config }; + + const transports: winston.transport[] = []; + + if (finalConfig.enableConsole) { + transports.push( + new winston.transports.Console({ + format: winston.format.combine( + winston.format.timestamp(), + winston.format.colorize(), + winston.format.printf(({ timestamp, level, message, service, ...meta }) => { + const serviceTag = service ? `[${service}]` : ''; + const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : ''; + return `${timestamp} ${level} ${serviceTag} ${message} ${metaStr}`; + }) + ), + }) + ); + } + + if (finalConfig.enableFile) { + transports.push( + new winston.transports.File({ + filename: `${finalConfig.logDir}/error.log`, + level: 'error', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + }), + new winston.transports.File({ + filename: `${finalConfig.logDir}/combined.log`, + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + }) + ); + } + + return winston.createLogger({ + level: finalConfig.level, + defaultMeta: { + service: finalConfig.serviceName, + }, + transports, + }); +}; + +export const logger = createLogger(); + +export default logger; diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json new file mode 100644 index 00000000..17085d1c --- /dev/null +++ b/packages/logger/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/tracing/README.md b/packages/tracing/README.md new file mode 100644 index 00000000..761b56d0 --- /dev/null +++ b/packages/tracing/README.md @@ -0,0 +1,16 @@ +# @goodgo/tracing + +OpenTelemetry tracing setup for distributed tracing. + +## Usage + +```typescript +import { initTracing } from '@goodgo/tracing'; + +// Initialize tracing at application startup +initTracing({ + serviceName: 'auth-service', + jaegerEndpoint: process.env.JAEGER_ENDPOINT, + enabled: process.env.TRACING_ENABLED === 'true', +}); +``` diff --git a/packages/tracing/package.json b/packages/tracing/package.json new file mode 100644 index 00000000..99e9e4df --- /dev/null +++ b/packages/tracing/package.json @@ -0,0 +1,28 @@ +{ + "name": "@goodgo/tracing", + "version": "1.0.0", + "description": "OpenTelemetry tracing setup", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist" + }, + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/sdk-node": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation-http": "^0.57.0", + "@opentelemetry/instrumentation-express": "^0.57.0", + "@opentelemetry/auto-instrumentations-node": "^0.52.0", + "@opentelemetry/resources": "^1.29.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "@opentelemetry/exporter-jaeger": "^1.28.0" + }, + "devDependencies": { + "@goodgo/tsconfig": "workspace:*", + "@types/node": "^20.11.0", + "typescript": "^5.3.3" + } +} diff --git a/packages/tracing/src/index.ts b/packages/tracing/src/index.ts new file mode 100644 index 00000000..cf071f73 --- /dev/null +++ b/packages/tracing/src/index.ts @@ -0,0 +1,37 @@ +import { NodeSDK } from '@opentelemetry/sdk-node'; +import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; +import { JaegerExporter } from '@opentelemetry/exporter-jaeger'; +import { Resource } from '@opentelemetry/resources'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; + +export interface TracingConfig { + serviceName: string; + jaegerEndpoint?: string; + enabled?: boolean; +} + +export const initTracing = (config: TracingConfig): NodeSDK | null => { + if (config.enabled === false) { + return null; + } + + const jaegerExporter = config.jaegerEndpoint + ? new JaegerExporter({ + endpoint: config.jaegerEndpoint, + }) + : undefined; + + const sdk = new NodeSDK({ + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: config.serviceName, + }), + traceExporter: jaegerExporter, + instrumentations: [getNodeAutoInstrumentations()], + }); + + sdk.start(); + + return sdk; +}; + +export default initTracing; diff --git a/packages/tracing/tsconfig.json b/packages/tracing/tsconfig.json new file mode 100644 index 00000000..6c2ca3d9 --- /dev/null +++ b/packages/tracing/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@goodgo/tsconfig/node.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/types/README.md b/packages/types/README.md new file mode 100644 index 00000000..65b780f5 --- /dev/null +++ b/packages/types/README.md @@ -0,0 +1,18 @@ +# @goodgo/types + +Shared TypeScript types and interfaces used across the monorepo. + +## Usage + +```typescript +import { User, Role, ApiResponse, LoginDto } from '@goodgo/types'; + +const user: User = { + id: '123', + email: 'user@example.com', + role: Role.USER, + isActive: true, + createdAt: new Date(), + updatedAt: new Date(), +}; +``` diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 00000000..5fd2dd4b --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,16 @@ +{ + "name": "@goodgo/types", + "version": "1.0.0", + "description": "Shared TypeScript types and interfaces", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "typescript": "^5.3.3" + } +} diff --git a/packages/types/src/api.types.ts b/packages/types/src/api.types.ts new file mode 100644 index 00000000..f21ca0f9 --- /dev/null +++ b/packages/types/src/api.types.ts @@ -0,0 +1,29 @@ +export interface ApiResponse { + success: boolean; + data?: T; + message?: string; + error?: ApiError; + timestamp: string; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} + +export interface PaginatedResponse extends ApiResponse { + pagination: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +} + +export interface PaginationQuery { + page?: number; + limit?: number; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; +} diff --git a/packages/types/src/auth.types.ts b/packages/types/src/auth.types.ts new file mode 100644 index 00000000..70ffcb8c --- /dev/null +++ b/packages/types/src/auth.types.ts @@ -0,0 +1,44 @@ +export interface LoginDto { + email: string; + password: string; +} + +export interface RegisterDto { + email: string; + password: string; + confirmPassword: string; +} + +import { UserResponse } from './user.types'; + +export interface AuthResponse { + accessToken: string; + refreshToken: string; + user: UserResponse; +} + +export interface RefreshTokenDto { + refreshToken: string; +} + +export interface TokenPayload { + userId: string; + email: string; + role: string; + iat?: number; + exp?: number; +} + +export interface ChangePasswordDto { + currentPassword: string; + newPassword: string; +} + +export interface ForgotPasswordDto { + email: string; +} + +export interface ResetPasswordDto { + token: string; + newPassword: string; +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts new file mode 100644 index 00000000..0fe2e7bc --- /dev/null +++ b/packages/types/src/index.ts @@ -0,0 +1,3 @@ +export * from './user.types'; +export * from './auth.types'; +export * from './api.types'; diff --git a/packages/types/src/user.types.ts b/packages/types/src/user.types.ts new file mode 100644 index 00000000..13ef4852 --- /dev/null +++ b/packages/types/src/user.types.ts @@ -0,0 +1,35 @@ +export enum Role { + USER = 'USER', + ADMIN = 'ADMIN', + SUPER_ADMIN = 'SUPER_ADMIN', +} + +export interface User { + id: string; + email: string; + role: Role; + isActive: boolean; + createdAt: Date; + updatedAt: Date; +} + +export interface CreateUserDto { + email: string; + password: string; + role?: Role; +} + +export interface UpdateUserDto { + email?: string; + role?: Role; + isActive?: boolean; +} + +export interface UserResponse { + id: string; + email: string; + role: Role; + isActive: boolean; + createdAt: string; + updatedAt: string; +} diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json new file mode 100644 index 00000000..17085d1c --- /dev/null +++ b/packages/types/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..c215a163 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,8464 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^20.11.0 + version: 20.19.27 + eslint: + specifier: ^8.56.0 + version: 8.57.1 + prettier: + specifier: ^3.2.4 + version: 3.7.4 + turbo: + specifier: ^1.11.2 + version: 1.13.4 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + + apps/web-admin: + dependencies: + '@goodgo/http-client': + specifier: workspace:* + version: link:../../packages/http-client + '@goodgo/types': + specifier: workspace:* + version: link:../../packages/types + axios: + specifier: ^1.6.5 + version: 1.13.2 + next: + specifier: ^14.1.0 + version: 14.2.35(react-dom@18.3.1)(react@18.3.1) + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + zustand: + specifier: ^4.4.7 + version: 4.5.7(@types/react@18.3.27)(react@18.3.1) + devDependencies: + '@goodgo/eslint-config': + specifier: workspace:* + version: link:../../packages/config/eslint-config + '@goodgo/prettier-config': + specifier: workspace:* + version: link:../../packages/config/prettier-config + '@goodgo/tsconfig': + specifier: workspace:* + version: link:../../packages/config/tsconfig + '@types/node': + specifier: ^20.11.0 + version: 20.19.27 + '@types/react': + specifier: ^18.2.48 + version: 18.3.27 + '@types/react-dom': + specifier: ^18.2.18 + version: 18.3.7(@types/react@18.3.27) + autoprefixer: + specifier: ^10.4.17 + version: 10.4.23(postcss@8.5.6) + eslint: + specifier: ^8.56.0 + version: 8.57.1 + eslint-config-next: + specifier: ^14.1.0 + version: 14.2.35(eslint@8.57.1)(typescript@5.9.3) + postcss: + specifier: ^8.4.33 + version: 8.5.6 + tailwindcss: + specifier: ^3.4.1 + version: 3.4.19 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + + apps/web-client: + dependencies: + '@goodgo/http-client': + specifier: workspace:* + version: link:../../packages/http-client + '@goodgo/types': + specifier: workspace:* + version: link:../../packages/types + axios: + specifier: ^1.6.5 + version: 1.13.2 + next: + specifier: ^14.1.0 + version: 14.2.35(react-dom@18.3.1)(react@18.3.1) + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + zustand: + specifier: ^4.4.7 + version: 4.5.7(@types/react@18.3.27)(react@18.3.1) + devDependencies: + '@goodgo/eslint-config': + specifier: workspace:* + version: link:../../packages/config/eslint-config + '@goodgo/prettier-config': + specifier: workspace:* + version: link:../../packages/config/prettier-config + '@goodgo/tsconfig': + specifier: workspace:* + version: link:../../packages/config/tsconfig + '@types/node': + specifier: ^20.11.0 + version: 20.19.27 + '@types/react': + specifier: ^18.2.48 + version: 18.3.27 + '@types/react-dom': + specifier: ^18.2.18 + version: 18.3.7(@types/react@18.3.27) + autoprefixer: + specifier: ^10.4.17 + version: 10.4.23(postcss@8.5.6) + eslint: + specifier: ^8.56.0 + version: 8.57.1 + eslint-config-next: + specifier: ^14.1.0 + version: 14.2.35(eslint@8.57.1)(typescript@5.9.3) + postcss: + specifier: ^8.4.33 + version: 8.5.6 + tailwindcss: + specifier: ^3.4.1 + version: 3.4.19 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + + packages/auth-sdk: + dependencies: + '@goodgo/types': + specifier: workspace:* + version: link:../types + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.3 + devDependencies: + '@goodgo/tsconfig': + specifier: workspace:* + version: link:../config/tsconfig + '@types/jsonwebtoken': + specifier: ^9.0.5 + version: 9.0.10 + '@types/node': + specifier: ^20.11.0 + version: 20.19.27 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + + packages/config/eslint-config: + dependencies: + '@typescript-eslint/eslint-plugin': + specifier: ^6.19.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.19.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.56.0 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.2(eslint@8.57.1) + eslint-plugin-import: + specifier: ^2.29.1 + version: 2.32.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + + packages/config/prettier-config: + dependencies: + prettier: + specifier: ^3.2.4 + version: 3.7.4 + + packages/config/tsconfig: {} + + packages/http-client: + dependencies: + '@goodgo/types': + specifier: workspace:* + version: link:../types + axios: + specifier: ^1.6.5 + version: 1.13.2 + devDependencies: + '@goodgo/tsconfig': + specifier: workspace:* + version: link:../config/tsconfig + '@types/node': + specifier: ^20.11.0 + version: 20.19.27 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + + packages/logger: + dependencies: + winston: + specifier: ^3.11.0 + version: 3.19.0 + devDependencies: + '@goodgo/tsconfig': + specifier: workspace:* + version: link:../config/tsconfig + '@types/node': + specifier: ^20.11.0 + version: 20.19.27 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + + packages/tracing: + dependencies: + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/auto-instrumentations-node': + specifier: ^0.52.0 + version: 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-jaeger': + specifier: ^1.28.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': + specifier: ^0.57.0 + version: 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-express': + specifier: ^0.57.0 + version: 0.57.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': + specifier: ^0.57.0 + version: 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: ^1.29.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': + specifier: ^0.57.0 + version: 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': + specifier: ^1.29.0 + version: 1.38.0 + devDependencies: + '@goodgo/tsconfig': + specifier: workspace:* + version: link:../config/tsconfig + '@types/node': + specifier: ^20.11.0 + version: 20.19.27 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + + packages/types: + devDependencies: + '@types/node': + specifier: ^20.11.0 + version: 20.19.27 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + + services/_template: + dependencies: + '@goodgo/auth-sdk': + specifier: workspace:* + version: link:../../packages/auth-sdk + '@goodgo/logger': + specifier: workspace:* + version: link:../../packages/logger + '@goodgo/tracing': + specifier: workspace:* + version: link:../../packages/tracing + '@goodgo/types': + specifier: workspace:* + version: link:../../packages/types + '@prisma/client': + specifier: ^5.9.1 + version: 5.22.0(prisma@5.22.0) + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^4.18.2 + version: 4.22.1 + express-rate-limit: + specifier: ^7.1.5 + version: 7.5.1(express@4.22.1) + helmet: + specifier: ^7.1.0 + version: 7.2.0 + zod: + specifier: ^3.22.4 + version: 3.25.76 + devDependencies: + '@goodgo/eslint-config': + specifier: workspace:* + version: link:../../packages/config/eslint-config + '@goodgo/tsconfig': + specifier: workspace:* + version: link:../../packages/config/tsconfig + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^4.17.21 + version: 4.17.25 + '@types/jest': + specifier: ^29.5.11 + version: 29.5.14 + '@types/node': + specifier: ^20.11.0 + version: 20.19.27 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@20.19.27) + prisma: + specifier: ^5.9.1 + version: 5.22.0 + ts-jest: + specifier: ^29.1.2 + version: 29.4.6(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.9.3) + tsx: + specifier: ^4.7.1 + version: 4.21.0 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + + services/auth-service: + dependencies: + '@goodgo/auth-sdk': + specifier: workspace:* + version: link:../../packages/auth-sdk + '@goodgo/logger': + specifier: workspace:* + version: link:../../packages/logger + '@goodgo/tracing': + specifier: workspace:* + version: link:../../packages/tracing + '@goodgo/types': + specifier: workspace:* + version: link:../../packages/types + '@prisma/client': + specifier: ^5.9.1 + version: 5.22.0(prisma@5.22.0) + bcryptjs: + specifier: ^2.4.3 + version: 2.4.3 + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^4.18.2 + version: 4.22.1 + express-rate-limit: + specifier: ^7.1.5 + version: 7.5.1(express@4.22.1) + helmet: + specifier: ^7.1.0 + version: 7.2.0 + ioredis: + specifier: ^5.3.2 + version: 5.8.2 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.3 + zod: + specifier: ^3.22.4 + version: 3.25.76 + devDependencies: + '@goodgo/eslint-config': + specifier: workspace:* + version: link:../../packages/config/eslint-config + '@goodgo/tsconfig': + specifier: workspace:* + version: link:../../packages/config/tsconfig + '@types/bcryptjs': + specifier: ^2.4.6 + version: 2.4.6 + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^4.17.21 + version: 4.17.25 + '@types/jest': + specifier: ^29.5.11 + version: 29.5.14 + '@types/jsonwebtoken': + specifier: ^9.0.5 + version: 9.0.10 + '@types/node': + specifier: ^20.11.0 + version: 20.19.27 + '@types/supertest': + specifier: ^2.0.16 + version: 2.0.16 + dotenv-cli: + specifier: ^11.0.0 + version: 11.0.0 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@20.19.27) + prisma: + specifier: ^5.9.1 + version: 5.22.0 + supertest: + specifier: ^6.3.3 + version: 6.3.4 + ts-jest: + specifier: ^29.1.2 + version: 29.4.6(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.9.3) + tsx: + specifier: ^4.7.1 + version: 4.21.0 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + +packages: + + /@alloc/quick-lru@5.2.0: + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + dev: true + + /@babel/code-frame@7.27.1: + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + dev: true + + /@babel/compat-data@7.28.5: + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.28.5: + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator@7.28.5: + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + dev: true + + /@babel/helper-compilation-targets@7.27.2: + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-globals@7.28.0: + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-module-imports@7.27.1: + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5): + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-plugin-utils@7.27.1: + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-string-parser@7.27.1: + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.28.5: + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.27.1: + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helpers@7.28.4: + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + dev: true + + /@babel/parser@7.28.5: + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.28.5 + dev: true + + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.5): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.5): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.5): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.5): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.5): + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.5): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.5): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5): + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.5): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.5): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.5): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.5): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.5): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.5): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.5): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.5): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.5): + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + dev: true + + /@babel/template@7.27.2: + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + dev: true + + /@babel/traverse@7.28.5: + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.28.5: + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + dev: true + + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true + + /@colors/colors@1.6.0: + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + dev: false + + /@dabh/diagnostics@2.0.8: + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + dev: false + + /@emnapi/core@1.7.1: + resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} + requiresBuild: true + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + /@emnapi/runtime@1.7.1: + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + requiresBuild: true + dependencies: + tslib: 2.8.1 + optional: true + + /@emnapi/wasi-threads@1.1.0: + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + requiresBuild: true + dependencies: + tslib: 2.8.1 + optional: true + + /@esbuild/aix-ppc64@0.27.2: + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.27.2: + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.27.2: + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.27.2: + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.27.2: + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.27.2: + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.27.2: + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.27.2: + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.27.2: + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.27.2: + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.27.2: + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.27.2: + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.27.2: + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.27.2: + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.27.2: + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.27.2: + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.27.2: + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-arm64@0.27.2: + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.27.2: + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-arm64@0.27.2: + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.27.2: + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openharmony-arm64@0.27.2: + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.27.2: + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.27.2: + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.27.2: + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.27.2: + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@eslint-community/eslint-utils@4.9.0(eslint@8.57.1): + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + /@eslint-community/regexpp@4.12.2: + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + /@eslint/eslintrc@2.1.4: + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + /@eslint/js@8.57.1: + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + /@grpc/grpc-js@1.14.3: + resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} + engines: {node: '>=12.10.0'} + dependencies: + '@grpc/proto-loader': 0.8.0 + '@js-sdsl/ordered-map': 4.4.2 + dev: false + + /@grpc/proto-loader@0.8.0: + resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} + engines: {node: '>=6'} + hasBin: true + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + dev: false + + /@humanwhocodes/config-array@0.13.0: + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + /@humanwhocodes/object-schema@2.0.3: + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + /@ioredis/commands@1.4.0: + resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==} + dev: false + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + + /@istanbuljs/load-nyc-config@1.1.0: + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.2 + resolve-from: 5.0.0 + dev: true + + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true + + /@jest/console@29.7.0: + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.19.27 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + dev: true + + /@jest/core@29.7.0: + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + 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 + 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': 20.19.27 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.19.27) + 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.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /@jest/environment@29.7.0: + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.27 + jest-mock: 29.7.0 + dev: true + + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + dev: true + + /@jest/expect@29.7.0: + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/fake-timers@29.7.0: + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 20.19.27 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + + /@jest/globals@29.7.0: + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/reporters@29.7.0: + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + 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 + 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.31 + '@types/node': 20.19.27 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.2.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + + /@jest/source-map@29.6.3: + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + dev: true + + /@jest/test-result@29.7.0: + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.3 + dev: true + + /@jest/test-sequencer@29.7.0: + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + dev: true + + /@jest/transform@29.7.0: + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.28.5 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 20.19.27 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + dev: true + + /@jridgewell/gen-mapping@0.3.13: + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + dev: true + + /@jridgewell/remapping@2.3.5: + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + dev: true + + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.5.5: + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + dev: true + + /@jridgewell/trace-mapping@0.3.31: + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + dev: true + + /@js-sdsl/ordered-map@4.4.2: + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + dev: false + + /@napi-rs/wasm-runtime@0.2.12: + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + requiresBuild: true + dependencies: + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + /@next/env@14.2.35: + resolution: {integrity: sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==} + dev: false + + /@next/eslint-plugin-next@14.2.35: + resolution: {integrity: sha512-Jw9A3ICz2183qSsqwi7fgq4SBPiNfmOLmTPXKvlnzstUwyvBrtySiY+8RXJweNAs9KThb1+bYhZh9XWcNOr2zQ==} + dependencies: + glob: 10.3.10 + dev: true + + /@next/swc-darwin-arm64@14.2.33: + resolution: {integrity: sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@next/swc-darwin-x64@14.2.33: + resolution: {integrity: sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-arm64-gnu@14.2.33: + resolution: {integrity: sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-arm64-musl@14.2.33: + resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-x64-gnu@14.2.33: + resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-x64-musl@14.2.33: + resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-arm64-msvc@14.2.33: + resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-ia32-msvc@14.2.33: + resolution: {integrity: sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-x64-msvc@14.2.33: + resolution: {integrity: sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@noble/hashes@1.8.0: + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + /@nolyfill/is-core-module@1.0.39: + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + /@opentelemetry/api-logs@0.208.0: + resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} + engines: {node: '>=8.0.0'} + dependencies: + '@opentelemetry/api': 1.9.0 + dev: false + + /@opentelemetry/api-logs@0.54.2: + resolution: {integrity: sha512-4MTVwwmLgUh5QrJnZpYo6YRO5IBLAggf2h8gWDblwRagDStY13aEvt7gGk3jewrMaPlHiF83fENhIx0HO97/cQ==} + engines: {node: '>=14'} + dependencies: + '@opentelemetry/api': 1.9.0 + dev: false + + /@opentelemetry/api-logs@0.57.2: + resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} + engines: {node: '>=14'} + dependencies: + '@opentelemetry/api': 1.9.0 + dev: false + + /@opentelemetry/api@1.9.0: + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + dev: false + + /@opentelemetry/auto-instrumentations-node@0.52.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-4QaRTZifSoYnh27B3JA7z7YwE0Nwkd824pDeonAQVijeLLsenhZB1japualZ6mF9lY8VdQId9KkNsgmCGdJVNQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.4.1 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-amqplib': 0.43.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-aws-lambda': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-aws-sdk': 0.46.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-bunyan': 0.42.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-cassandra-driver': 0.42.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-connect': 0.40.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-cucumber': 0.10.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dataloader': 0.13.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dns': 0.40.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-express': 0.44.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fastify': 0.41.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fs': 0.16.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-generic-pool': 0.40.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-graphql': 0.44.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-grpc': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-hapi': 0.42.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-ioredis': 0.44.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-kafkajs': 0.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-knex': 0.41.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-koa': 0.44.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-lru-memoizer': 0.41.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-memcached': 0.40.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongodb': 0.48.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongoose': 0.43.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql': 0.42.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql2': 0.42.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-nestjs-core': 0.41.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-net': 0.40.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pg': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pino': 0.43.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis': 0.43.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis-4': 0.43.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-restify': 0.42.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-router': 0.41.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-socket.io': 0.43.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-tedious': 0.15.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-undici': 0.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-winston': 0.41.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-alibaba-cloud': 0.29.7(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-aws': 1.12.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-azure': 0.2.12(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-container': 0.5.3(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-gcp': 0.29.13(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': 0.54.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /@opentelemetry/context-async-hooks@1.27.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-CdZ3qmHCwNhFAzjTgHqrDQ44Qxcpz43cVxZRhOs+Ns/79ug+Mr84Bkb626bkJLkA3+BLimA5YAEVRlJC6pFb7g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + dev: false + + /@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + dev: false + + /@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-yQPKnK5e+76XuiqUH/gKyS8wv/7qITd5ln56QkBTf3uggr0VkXOXfcaAuG330UfdYu83wsyoBwqwxigpIG+Jkg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.27.0 + dev: false + + /@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.28.0 + dev: false + + /@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.38.0 + dev: false + + /@opentelemetry/exporter-jaeger@1.30.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-7Ki+x7cZ/PEQxp3UyB+CWkWBqLk22yRGQ4AWIGwZlEs6rpCOdWwIFOyQDO9DdeyWtTPTvO3An/7chPZcRHOgzQ==} + engines: {node: '>=14'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + jaeger-client: 3.19.0 + dev: false + + /@opentelemetry/exporter-logs-otlp-grpc@0.54.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-MQNmV5r96+5n3axLFgNYtVy62x8Ru7VERZH3zgC50KDcIKWCiQT3vHOtzakhzd1Wq0HqOgu6bzKdwzneSoDrEQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.54.2(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/exporter-logs-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-eovEy10n3umjKJl2Ey6TLzikPE+W4cUQ4gCwgGP1RqzTGtgDra0WjIqdy29ohiUKfvmbiL3MndZww58xfIvyFw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/exporter-logs-otlp-http@0.54.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-wYeCSbX2XWX2wFslnfQ/YFUolO0fj2nUiGI7oEQWpLKSg40Lc4xOOW14X/EXOkCCijhP7bigo6nvyEQlxEVLjA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.54.2 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.54.2(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/exporter-logs-otlp-http@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-0rygmvLcehBRp56NQVLSleJ5ITTduq/QfU7obOkyWgPpFHulwpw2LYTqNIz5TczKZuy5YY+5D3SDnXZL1tXImg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/exporter-logs-otlp-proto@0.54.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-agrzFbSNmIy6dhkyg41ERlEDUDqkaUJj2n/tVRFp9Tl+6wyNVPsqmwU5RWJOXpyK+lYH/znv6A47VpTeJF0lrw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.54.2 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/exporter-logs-otlp-proto@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-ta0ithCin0F8lu9eOf4lEz9YAScecezCHkMMyDkvd9S7AnZNX5ikUmC5EQOQADU+oCcgo/qkQIaKcZvQ0TYKDw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/exporter-metrics-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-r70B8yKR41F0EC443b5CGB4rUaOMm99I5N75QQt6sHKxYDzSEc6gm48Diz1CI1biwa5tDPznpylTrywO/pT7qw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/exporter-metrics-otlp-http@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-ttb9+4iKw04IMubjm3t0EZsYRNWr3kg44uUuzfo9CaccYlOh8cDooe4QObDUkvx9d5qQUrbEckhrWKfJnKhemA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/exporter-metrics-otlp-proto@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-HX068Q2eNs38uf7RIkNN9Hl4Ynl+3lP0++KELkXMCpsCbFO03+0XNNZ1SkwxPlP9jrhQahsMPMkzNXpq3fKsnw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/exporter-prometheus@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-VqIqXnuxWMWE/1NatAGtB1PvsQipwxDcdG4RwA/umdBcW3/iOHp0uejvFHTRN2O78ZPged87ErJajyUBPUhlDQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/exporter-trace-otlp-grpc@0.54.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-tmxiCYhQdPrzwlM6O7VQeNP9PBjKhaiOo54wFxQFZQcoVaDiOOES4+6PwHU1eW+43mDsgdQHN5AHSRHVLe9jDA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/exporter-trace-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-gHU1vA3JnHbNxEXg5iysqCWxN9j83d7/epTYBZflqQnTyCC4N7yZXn/dMM+bEmyhQPGjhCkNZLx4vZuChH1PYw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/exporter-trace-otlp-http@0.54.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-BgWKKyD/h2zpISdmYHN/sapwTjvt1P4p5yx4xeBV8XAEqh4OQUhOtSGFG80+nPQ1F8of3mKOT1DDoDbJp1u25w==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/exporter-trace-otlp-http@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-sB/gkSYFu+0w2dVQ0PWY9fAMl172PKMZ/JrHkkW8dmjCL0CYkmXeE+ssqIL/yBUTPOvpLIpenX5T9RwXRBW/3g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/exporter-trace-otlp-proto@0.54.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-XSmm1N2wAhoWDXP1q/N6kpLebWaxl6VIADv4WA5QWKHLRpF3gLz5NAWNJBR8ygsvv8jQcrwnXgwfnJ18H3v1fg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/exporter-trace-otlp-proto@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-awDdNRMIwDvUtoRYxRhja5QYH6+McBLtoz1q9BeEsskhZcrGmH/V1fWpGx8n+Rc+542e8pJA6y+aullbIzQmlw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/exporter-zipkin@1.27.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-eGMY3s4QprspFZojqsuQyQpWNFpo+oNVE/aosTbtvAlrJBAlvXcwwsOROOHOd8Y9lkU4i0FpQW482rcXkgwCSw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + dev: false + + /@opentelemetry/exporter-zipkin@1.30.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-6S2QIMJahIquvFaaxmcwpvQQRD/YFaMTNoIxrfPIPOeITN+a8lfEcPDxNxn8JDAaxkg+4EnXhz8upVDYenoQjA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + dev: false + + /@opentelemetry/instrumentation-amqplib@0.43.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-ALjfQC+0dnIEcvNYsbZl/VLh7D2P1HhFF4vicRKHhHFIUV3Shpg4kXgiek5PLhmeKSIPiUB25IYH5RIneclL4A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-aws-lambda@0.47.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-0BidKDPziHWGl5mnpLuh7ob1X3KpR0UN3QcJkcxIsOMylBbMMp9EoB55dHsTMoNO7bx2uyeY0iirEuTchjF1gQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + '@types/aws-lambda': 8.10.143 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-aws-sdk@0.46.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-EyxGQVYhgY8OI4/CKzqamUswiEVlua6DJcsmkeNSykZrDGs78jPfssbqoMQGetywHWPZBRVJN4Ba/7aB5iLHBA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/propagation-utils': 0.30.16(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-bunyan@0.42.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-GBh6ybwKmFZjc86SyHVx72jHg+4pFPaXT3IZgJ4QtnMsMf0/q5m2aHAjid+yakmEkApsnRWX8pJ8nkl1e+6mag==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.54.2 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@types/bunyan': 1.8.9 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-cassandra-driver@0.42.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-35I9Gw4BeSs9NPe7fugu9e/mWKaapc/N1wounHnGt259/Q3ISGMOQRrOwIBw+x/XJygJvn4Ss1c+r5h89TsVAw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-connect@0.40.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-3aR/3YBQ160siitwwRLjwqrv2KBT16897+bo6yz8wIfel6nWOxTZBJudcbsK3p42pTC7qrbotJ9t/1wRLpv79Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + '@types/connect': 3.4.36 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-cucumber@0.10.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-5sT6Ap3W7StEL0Oax/vd1YTEcTPTefx+9myzkKrr72hxzFzSooGRCxlU3sfPwZqWptUV7+QWTMd7SqGEEPnE/w==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-dataloader@0.13.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-wbU3WdgUAXljEIY2nfpkqID/VH70ThnES8mZZHKCZlV/Pl5T4+qmrVdT7U9/WUzz8flwsXfER6T6jl48Wbl+LQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-dns@0.40.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-tLNR8XLPiYRKKk3/UqifXnPP2TVt1RcwvHU0R1ETL1xkZ1ZHMTmSC4x6TignnHOFtRixtJ05EgMGejnffaBXkQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-express@0.44.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-GWgibp6Q0wxyFaaU8ERIgMMYgzcHmGrw3ILUtGchLtLncHNOKk0SNoWGqiylXWWT4HTn5XdV8MGawUgpZh80cA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-express@0.57.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-r+ulPbvgG8rGgFFWbJWJpTh7nMzsEYH7rBFNWdFs7ZfVAtgpFijMkRtU7DecIo6ItF8Op+RxogSuk/083W8HKw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-fastify@0.41.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-pNRjFvf0mvqfJueaeL/qEkuGJwgtE5pgjIHGYwjc2rMViNCrtY9/Sf+Nu8ww6dDd/Oyk2fwZZP7i0XZfCnETrA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-fs@0.16.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-hMDRUxV38ln1R3lNz6osj3YjlO32ykbHqVrzG7gEhGXFQfu7LJUx8t9tEwE4r2h3CD4D0Rw4YGDU4yF4mP3ilg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-generic-pool@0.40.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-k+/JlNDHN3bPi/Cir+Ew6tKHFVCa1ZFeQyGUw5HQkRX/twCRaN3kJFXJW+rDAN90XwK3RtC9AWwBihDGh/oSlQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-graphql@0.44.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-FYXTe3Bv96aNpYktqm86BFUTpjglKD0kWI5T5bxYkLUPEPvFn38vWGMJTGrDMVou/i55E4jlWvcm6hFIqLsMbg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-grpc@0.54.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-KhSzerCaaqVH2zfDro7nTunWUZXt1pQISQpE83LuQTOKGk7mN3G60T1wliQ3Qdg0X3UUuhCXEC7u6IAVfDxkUQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-hapi@0.42.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-TQC0BtIWLHrp6nKsYdZ5t5B7aiZ16BwbRqZtYYQxeJVsq/HQTANWpknjtA7KMxv5tAUMCrU/eDo8F3qioUOSZg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-http@0.54.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-mABjJ34UcU32pg8g18L9xBh0U3JON/2F6/57BYYy8AZJp2a71lZjcKr0T00pICoic50TW5HvcTrmyfMil+AiXQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + forwarded-parse: 2.1.2 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-http@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-1Uz5iJ9ZAlFOiPuwYg29Bf7bJJc/GeoeJIFKJYQf67nTVKFe8RHbEtxgkOmK4UGZNHKXcpW4P8cWBYzBn1USpg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + forwarded-parse: 2.1.2 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-ioredis@0.44.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-312pE2xc0ihX9haTf9WC4OF9in5EfVO1y5I8Ef9aMQKJNhuSe3IgzQAqGoLfaYajC+ig0IZ9SQKU8mRbFwHU+A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-kafkajs@0.4.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-I9VwDG314g7SDL4t8kD/7+1ytaDBRbZQjhVaQaVIDR8K+mlsoBhLsWH79yHxhHQKvwCSZwqXF+TiTOhoQVUt7A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-knex@0.41.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-OhI1SlLv5qnsnm2dOVrian/x3431P75GngSpnR7c4fcVFv7prXGYu29Z6ILRWJf/NJt6fkbySmwdfUUnFnHCTg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-koa@0.44.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-ryPqGIQ4hpMGd85bAGjRMDAy/ic+Qdh1GtFGJo9KaXdzbcvZoF1ZgXVsKTYDxbD1n5C0BoQy6rcWg8Lu68iCJA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-lru-memoizer@0.41.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-6OePkk4RYCPVsnS0TroEK6UZzxxxjVWaE6EPdOn2qxGHMtm+Qb80tiBQ6BbmC+f7bjc27O85JY8gxeTybhHZXw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-memcached@0.40.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-VzJUUH6cVz8yrb25RvvjhxCpwu4vUk28I0m5nnnhebULOo8p9lda5PgQeVde2+jQAd977C/vN714fkbYOmwb+A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + '@types/memcached': 2.2.10 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-mongodb@0.48.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-9YWvaGvrrcrydMsYGLu0w+RgmosLMKe3kv/UNlsPy8RLnCkN2z+bhhbjjjuxtUmvEuKZMCoXFluABVuBr1yhjw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-mongoose@0.43.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-y1mWuL/zb6IKi199HkROgmStxF/ybEsnKYgx+/lpLATd57oZHOqrXP9tLmp9qRVI5c6P5XEWfe7ZCvrj07iDMQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-mysql2@0.42.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-5hOQbFSpqsgDLaqIeWZNbSWB6XdwN+aBjoCIe60lmGG86zeNXu9I6l1kEckRb+Gy0i7zrt0Tk8S62zsOSZ8l7Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-mysql@0.42.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-1GN2EBGVSZABGQ25MSz3faeBW/DwhzmE10aNW1/A2mvQAxF1CvpMk17YmNUzwapVt29iKsiU3SXQG7vjh/019A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + '@types/mysql': 2.15.26 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-nestjs-core@0.41.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-XCqtghFktpcJ2BOaJtFfqtTMsHffJADxfYhJl28WT6ygCChS2uZVxMKKLsy+i9VtPaw/i1IumPICL6mbhwq+Vw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-net@0.40.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-abErnVRxTmtiF7EvBISW81Se2nj/j3Xtpfy//9++dgvDOXwbcD1Xz1via6ZHOm/VamboGhqPlYiO7ABzluPLwg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-pg@0.47.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-qIcydMBVlKtAyFQWYunjqvFMVqIGvxGMXISrdLuSbcCqico9QKhK7bF5wzsotjGwHcGnc7q5kRqSL7j+LnY1Cw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) + '@types/pg': 8.6.1 + '@types/pg-pool': 2.0.6 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-pino@0.43.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-jlOOgbODWRRNknWXY1VLgmqgG0SO4kLgU3XnejjO/3De4OisroAsMGk+1cRB5AQ6WZ8WLAMkMyTShaOe6j2Asw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.54.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-redis-4@0.43.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-6B2+CFRY9xRnkeZrSvlTyY2yB/zAgxjbXS5EwXhE3ZAKR1hWWoUzaTADIKT5xe9/VbDW42U3UoOPCcaCmeAXww==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-redis@0.43.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-dufe08W3sCOjutbTJmV6tg2Y3+7IBe59oQrnIW2RCgjRhsW0Jjaenezt490eawO0MdXjUfFyrIUg8WetKhE4xA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-restify@0.42.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-ApDD9HNy6de6xrHmISEfkQHwwX1f1JrBj0ADnlk6tVdJ0j/vNmsZNLwaU2IA2K3mHqbp2YLarLgxAZp6rjcfWg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-router@0.41.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-IbvzgaoylMqStOOtwucEvSu5CDbfQN+H1ZZ2p6c9Kmvzptqh6G441GFy0FFVVqxOAHNhQm2w6n0Ag8trdBjCfw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-socket.io@0.43.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-HAQoIZ6N/ey1L4jF69gmqo7RyeSv5rc4sZZAd1v6SVaB8ZolTEyWEzGlu1NRZZTnqfWNxDkX6J1/omWpDd9k0w==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-tedious@0.15.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-Kb7yo8Zsq2TUwBbmwYgTAMPK0VbhoS8ikJ6Bup9KrDtCx2JC01nCb+M0VJWXt7tl0+5jARUbKWh5jRSoImxdCw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-undici@0.7.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-sIl4zrRDP7pR+2Pmdm9XJQULMKiUmvZze2cEW6gUz7TXCEaYmJ+vNMdd7qgeRo8C7AMm+T08mptobFVKPzdz+A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation-winston@0.41.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-qtqGDx2Plu71s9xaeXut0YgZFG/y68ENG9vvo/SODeEC+4/APiS/htQ5YNJIxxjOuxYowdFYRqV9Kmef2EUzmw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.54.2 + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + import-in-the-middle: 2.0.1 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-go6zpOVoZVztT9r1aPd79Fr3OWiD4N24bCPJsIKkBses8oyFo12F/Ew3UBTdIu6hsW4HC4MVEJygG6TEyJI/lg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.54.2 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.15.0 + require-in-the-middle: 7.5.2 + semver: 7.7.3 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.15.0 + require-in-the-middle: 7.5.2 + semver: 7.7.3 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/otlp-exporter-base@0.54.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-NrNyxu6R/bGAwanhz1HI0aJWKR6xUED4TjCH4iWMlAfyRukGbI9Kt/Akd2sYLwRKNhfS+sKetKGCUQPMDyYYMA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.54.2(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/otlp-exporter-base@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-XdxEzL23Urhidyebg5E6jZoaiW5ygP/mRjxLHixogbqwDy2Faduzb5N0o/Oi+XTIJu+iyxXdVORjXax+Qgfxag==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/otlp-grpc-exporter-base@0.54.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-HZtACQuLhgDcgNa9arGnVVGV28sSGQ+iwRgICWikFKiVxUsoWffqBvTxPa6G3DUTg5R+up97j/zxubEyxSAOHg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.54.2(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/otlp-grpc-exporter-base@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-USn173KTWy0saqqRB5yU9xUZ2xdgb1Rdu5IosJnm9aV4hMTuFFRTUsQxbgc24QxpCHeoKzzCSnS/JzdV0oM2iQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/otlp-transformer@0.54.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-2tIjahJlMRRUz0A2SeE+qBkeBXBFkSjR0wqJ08kuOqaL8HNGan5iZf+A8cfrfmZzPUuMKCyY9I+okzFuFs6gKQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.54.2 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + dev: false + + /@opentelemetry/otlp-transformer@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + dev: false + + /@opentelemetry/propagation-utils@0.30.16(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-ZVQ3Z/PQ+2GQlrBfbMMMT0U7MzvYZLCPP800+ooyaBqm4hMvuQHfP028gB9/db0mwkmyEAMad9houukUVxhwcw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.9.0 + dev: false + + /@opentelemetry/propagator-b3@1.27.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-pTsko3gnMioe3FeWcwTQR3omo5C35tYsKKwjgTCTVCgd3EOWL9BZrMfgLBmszrwXABDfUrlAEFN/0W0FfQGynQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/propagator-b3@1.30.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/propagator-jaeger@1.27.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-EI1bbK0wn0yIuKlc2Qv2LKBRw6LiUWevrjCF80fn/rlaB+7StAi8Y5s8DBqAYNpY7v1q86+NjU18v7hj2ejU3A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/propagator-jaeger@1.30.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/redis-common@0.36.2: + resolution: {integrity: sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==} + engines: {node: '>=14'} + dev: false + + /@opentelemetry/resource-detector-alibaba-cloud@0.29.7(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-PExUl/R+reSQI6Y/eNtgAsk6RHk1ElYSzOa8/FHfdc/nLmx9sqMasBEpLMkETkzDP7t27ORuXe4F9vwkV2uwwg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + dev: false + + /@opentelemetry/resource-detector-aws@1.12.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-Cvi7ckOqiiuWlHBdA1IjS0ufr3sltex2Uws2RK6loVp4gzIJyOijsddAI6IZ5kiO8h/LgCWe8gxPmwkTKImd+Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + dev: false + + /@opentelemetry/resource-detector-azure@0.2.12(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-iIarQu6MiCjEEp8dOzmBvCSlRITPFTinFB2oNKAjU6xhx8d7eUcjNOKhBGQTvuCriZrxrEvDaEEY9NfrPQ6uYQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + dev: false + + /@opentelemetry/resource-detector-container@0.5.3(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-x5DxWu+ZALBuFpxwO2viv9ktH4Y3Gk9LaYKn2U8J+aeD412iy/OcGLPbQ76Px7pQ8qaJ5rnjcevBOHYT4aA+zQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + dev: false + + /@opentelemetry/resource-detector-gcp@0.29.13(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-vdotx+l3Q+89PeyXMgKEGnZ/CwzwMtuMi/ddgD9/5tKZ08DfDGB2Npz9m2oXPHRCjc4Ro6ifMqFlRyzIvgOjhg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + gcp-metadata: 6.1.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /@opentelemetry/resources@1.27.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-jOwt2VJ/lUD5BLc+PMNymDrUCpm5PKi1E9oSVYAvz01U/VdndGmrtV3DU1pG4AwlYhJRHbHfOUIlpBeXCPw6QQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + dev: false + + /@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + dev: false + + /@opentelemetry/sdk-logs@0.54.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-yIbYqDLS/AtBbPjCjh6eSToGNRMqW2VR8RrKEy+G+J7dFG7pKoptTH5T+XlKPleP9NY8JZYIpgJBlI+Osi0rFw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.54.2 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/sdk-metrics@1.27.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-JzWgzlutoXCydhHWIbLg+r76m+m3ncqvkCcsswXAQ4gqKS+LOHKhq+t6fx1zNytvLuaOUBur7EvWxECc4jPQKg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + dev: false + + /@opentelemetry/sdk-node@0.54.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-afn8GBpA7Gb55aU0LUxIQ+oe6QxLhsf+Te9iw12Non3ZAspzdoCcfz5+hqecwpuVpEDdnj5iSalF7VVaL2pDeg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.54.2 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/sdk-node@0.57.2(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-8BaeqZyN5sTuPBtAoY+UtKwXBdqyuRKmekN5bFzAO40CgbGzAxfTpiL3PBerT7rhZ7p2nBdq7FaMv/tBQgHE4A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-btz6XTQzwsyJjombpeqCX6LhiMQYpzt2pIYNPnw0IPO/3AhT6yjnf8Mnv3ZC2A4eRYOjqrg+bfaXg9XHDRJDWQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + dev: false + + /@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + dev: false + + /@opentelemetry/sdk-trace-node@1.27.0(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-dWZp/dVGdUEfRBjBq2BgNuBlFqHCxyyMc8FsN0NX15X07mxSUO0SZRLyK/fdAVrde8nqFI/FEdMH4rgU9fqJfQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0) + semver: 7.7.3 + dev: false + + /@opentelemetry/sdk-trace-node@1.30.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + semver: 7.7.3 + dev: false + + /@opentelemetry/semantic-conventions@1.27.0: + resolution: {integrity: sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==} + engines: {node: '>=14'} + dev: false + + /@opentelemetry/semantic-conventions@1.28.0: + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + dev: false + + /@opentelemetry/semantic-conventions@1.38.0: + resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==} + engines: {node: '>=14'} + dev: false + + /@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + dev: false + + /@paralleldrive/cuid2@2.3.1: + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + dependencies: + '@noble/hashes': 1.8.0 + dev: true + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + + /@prisma/client@5.22.0(prisma@5.22.0): + resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} + engines: {node: '>=16.13'} + requiresBuild: true + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + dependencies: + prisma: 5.22.0 + dev: false + + /@prisma/debug@5.22.0: + resolution: {integrity: sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==} + + /@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2: + resolution: {integrity: sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==} + + /@prisma/engines@5.22.0: + resolution: {integrity: sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==} + requiresBuild: true + dependencies: + '@prisma/debug': 5.22.0 + '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 + '@prisma/fetch-engine': 5.22.0 + '@prisma/get-platform': 5.22.0 + + /@prisma/fetch-engine@5.22.0: + resolution: {integrity: sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==} + dependencies: + '@prisma/debug': 5.22.0 + '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 + '@prisma/get-platform': 5.22.0 + + /@prisma/get-platform@5.22.0: + resolution: {integrity: sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==} + dependencies: + '@prisma/debug': 5.22.0 + + /@protobufjs/aspromise@1.1.2: + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + dev: false + + /@protobufjs/base64@1.1.2: + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + dev: false + + /@protobufjs/codegen@2.0.4: + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + dev: false + + /@protobufjs/eventemitter@1.1.0: + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + dev: false + + /@protobufjs/fetch@1.1.0: + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + dev: false + + /@protobufjs/float@1.0.2: + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + dev: false + + /@protobufjs/inquire@1.1.0: + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + dev: false + + /@protobufjs/path@1.1.2: + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + dev: false + + /@protobufjs/pool@1.1.0: + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + dev: false + + /@protobufjs/utf8@1.1.0: + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + dev: false + + /@rtsao/scc@1.1.0: + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + /@rushstack/eslint-patch@1.15.0: + resolution: {integrity: sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==} + dev: true + + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + + /@sinonjs/commons@3.0.1: + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/fake-timers@10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.1 + dev: true + + /@so-ric/colorspace@1.1.6: + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + dependencies: + color: 5.0.3 + text-hex: 1.0.0 + dev: false + + /@swc/counter@0.1.3: + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + dev: false + + /@swc/helpers@0.5.5: + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + dependencies: + '@swc/counter': 0.1.3 + tslib: 2.8.1 + dev: false + + /@tybys/wasm-util@0.10.1: + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + requiresBuild: true + dependencies: + tslib: 2.8.1 + optional: true + + /@types/aws-lambda@8.10.143: + resolution: {integrity: sha512-u5vzlcR14ge/4pMTTMDQr3MF0wEe38B2F9o84uC4F43vN5DGTy63npRrB6jQhyt+C0lGv4ZfiRcRkqJoZuPnmg==} + dev: false + + /@types/babel__core@7.20.5: + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + dev: true + + /@types/babel__generator@7.27.0: + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + dependencies: + '@babel/types': 7.28.5 + dev: true + + /@types/babel__template@7.4.4: + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + dev: true + + /@types/babel__traverse@7.28.0: + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + dependencies: + '@babel/types': 7.28.5 + dev: true + + /@types/bcryptjs@2.4.6: + resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==} + dev: true + + /@types/body-parser@1.19.6: + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.19.27 + dev: true + + /@types/bunyan@1.8.9: + resolution: {integrity: sha512-ZqS9JGpBxVOvsawzmVt30sP++gSQMTejCkIAQ3VdadOcRE8izTyW66hufvwLeH+YEGP6Js2AW7Gz+RMyvrEbmw==} + dependencies: + '@types/node': 20.19.27 + dev: false + + /@types/connect@3.4.36: + resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} + dependencies: + '@types/node': 20.19.27 + dev: false + + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + dependencies: + '@types/node': 20.19.27 + dev: true + + /@types/cookiejar@2.1.5: + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + dev: true + + /@types/cors@2.8.19: + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + dependencies: + '@types/node': 20.19.27 + dev: true + + /@types/express-serve-static-core@4.19.7: + resolution: {integrity: sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==} + dependencies: + '@types/node': 20.19.27 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + dev: true + + /@types/express@4.17.25: + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.7 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.10 + dev: true + + /@types/graceful-fs@4.1.9: + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + dependencies: + '@types/node': 20.19.27 + dev: true + + /@types/http-errors@2.0.5: + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + dev: true + + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + dev: true + + /@types/istanbul-lib-report@3.0.3: + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + dev: true + + /@types/istanbul-reports@3.0.4: + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + dependencies: + '@types/istanbul-lib-report': 3.0.3 + dev: true + + /@types/jest@29.5.14: + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + dev: true + + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + /@types/jsonwebtoken@9.0.10: + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + dependencies: + '@types/ms': 2.1.0 + '@types/node': 20.19.27 + dev: true + + /@types/memcached@2.2.10: + resolution: {integrity: sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==} + dependencies: + '@types/node': 20.19.27 + dev: false + + /@types/methods@1.1.4: + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + dev: true + + /@types/mime@1.3.5: + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + dev: true + + /@types/ms@2.1.0: + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + dev: true + + /@types/mysql@2.15.26: + resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} + dependencies: + '@types/node': 20.19.27 + dev: false + + /@types/node@20.19.27: + resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==} + dependencies: + undici-types: 6.21.0 + + /@types/pg-pool@2.0.6: + resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} + dependencies: + '@types/pg': 8.6.1 + dev: false + + /@types/pg@8.6.1: + resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} + dependencies: + '@types/node': 20.19.27 + pg-protocol: 1.10.3 + pg-types: 2.2.0 + dev: false + + /@types/prop-types@15.7.15: + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + /@types/qs@6.14.0: + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + dev: true + + /@types/range-parser@1.2.7: + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + dev: true + + /@types/react-dom@18.3.7(@types/react@18.3.27): + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + dependencies: + '@types/react': 18.3.27 + dev: true + + /@types/react@18.3.27: + resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + /@types/semver@7.7.1: + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + + /@types/send@0.17.6: + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.19.27 + dev: true + + /@types/send@1.2.1: + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + dependencies: + '@types/node': 20.19.27 + dev: true + + /@types/serve-static@1.15.10: + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 20.19.27 + '@types/send': 0.17.6 + dev: true + + /@types/shimmer@1.2.0: + resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + dev: false + + /@types/stack-utils@2.0.3: + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + dev: true + + /@types/superagent@8.1.9: + resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 20.19.27 + form-data: 4.0.5 + dev: true + + /@types/supertest@2.0.16: + resolution: {integrity: sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==} + dependencies: + '@types/superagent': 8.1.9 + dev: true + + /@types/tedious@4.0.14: + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + dependencies: + '@types/node': 20.19.27 + dev: false + + /@types/triple-beam@1.3.5: + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + dev: false + + /@types/yargs-parser@21.0.3: + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + dev: true + + /@types/yargs@17.0.35: + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + dependencies: + '@types/yargs-parser': 21.0.3 + dev: true + + /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1)(typescript@5.9.3): + resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.3 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + semver: 7.7.3 + ts-api-utils: 1.4.3(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + /@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3): + resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.3 + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + /@typescript-eslint/scope-manager@6.21.0: + resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + + /@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.9.3): + resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + debug: 4.4.3 + eslint: 8.57.1 + ts-api-utils: 1.4.3(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + /@typescript-eslint/types@6.21.0: + resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} + engines: {node: ^16.0.0 || >=18.0.0} + + /@typescript-eslint/typescript-estree@6.21.0(typescript@5.9.3): + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.7.3 + ts-api-utils: 1.4.3(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + /@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.9.3): + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@types/json-schema': 7.0.15 + '@types/semver': 7.7.1 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) + eslint: 8.57.1 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + - typescript + + /@typescript-eslint/visitor-keys@6.21.0: + resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.21.0 + eslint-visitor-keys: 3.4.3 + + /@ungap/structured-clone@1.3.0: + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + /@unrs/resolver-binding-android-arm-eabi@1.11.1: + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-android-arm64@1.11.1: + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-darwin-arm64@1.11.1: + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-darwin-x64@1.11.1: + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-freebsd-x64@1.11.1: + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1: + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-linux-arm-musleabihf@1.11.1: + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-linux-arm64-gnu@1.11.1: + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-linux-arm64-musl@1.11.1: + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-linux-ppc64-gnu@1.11.1: + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-linux-riscv64-gnu@1.11.1: + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-linux-riscv64-musl@1.11.1: + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-linux-s390x-gnu@1.11.1: + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-linux-x64-gnu@1.11.1: + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-linux-x64-musl@1.11.1: + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-wasm32-wasi@1.11.1: + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + requiresBuild: true + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + /@unrs/resolver-binding-win32-arm64-msvc@1.11.1: + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-win32-ia32-msvc@1.11.1: + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true + + /@unrs/resolver-binding-win32-x64-msvc@1.11.1: + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + + /acorn-import-attributes@1.9.5(acorn@8.15.0): + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + dependencies: + acorn: 8.15.0 + dev: false + + /acorn-jsx@5.3.2(acorn@8.15.0): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.15.0 + + /acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + /agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + dev: false + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + /ansi-color@0.2.2: + resolution: {integrity: sha512-qPx7iZZDHITYrrfzaUFXQpIcF2xYifcQHQflP1pFz8yY3lfU6GgCHb0+hJD7nimYKO7f2iaYYwBpZ+GaNcAhcA==} + dev: false + + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + /ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + + /ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + dev: true + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + /aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + dev: true + + /array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: false + + /array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + /array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + dev: true + + /array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + /array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + /array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + /array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + dev: true + + /arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + /asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + dev: true + + /ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + dev: true + + /async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + /async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + dev: false + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + /autoprefixer@10.4.23(postcss@8.5.6): + resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001761 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + dev: true + + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.1.0 + + /axe-core@4.11.0: + resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==} + engines: {node: '>=4'} + dev: true + + /axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + + /axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + dev: true + + /babel-jest@29.7.0(@babel/core@7.28.5): + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.28.5 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.28.5) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + dev: true + + /babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.5): + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.5) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.5) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.5) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.5) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.5) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.5) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.5) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.5) + dev: true + + /babel-preset-jest@29.6.3(@babel/core@7.28.5): + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.28.5 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /baseline-browser-mapping@2.9.11: + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} + hasBin: true + dev: true + + /bcryptjs@2.4.3: + resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} + dev: false + + /bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + dev: false + + /binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + dev: true + + /body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + 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 + transitivePeerDependencies: + - supports-color + dev: false + + /brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + /brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + dependencies: + balanced-match: 1.0.2 + + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.1.1 + + /browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001761 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + dev: true + + /bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + dependencies: + fast-json-stable-stringify: 2.1.0 + dev: true + + /bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + dependencies: + node-int64: 0.4.0 + dev: true + + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /bufrw@1.4.0: + resolution: {integrity: sha512-sWm8iPbqvL9+5SiYxXH73UOkyEbGQg7kyHQmReF89WJHQJw2eV4P/yZ0E+b71cczJ4pPobVhXxgQcmfSTgGHxQ==} + engines: {node: '>= 0.10.x'} + dependencies: + ansi-color: 0.2.2 + error: 7.0.2 + hexer: 1.5.0 + xtend: 4.0.2 + dev: false + + /busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: false + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + + /call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + /call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + /call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: true + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: true + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + dev: true + + /caniuse-lite@1.0.30001761: + resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + dev: true + + /chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + dev: true + + /cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + /client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + dev: false + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + /cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + dev: false + + /co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + dev: true + + /collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + + /color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + dependencies: + color-name: 2.1.0 + dev: false + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + /color-name@2.1.0: + resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + engines: {node: '>=12.20'} + dev: false + + /color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} + dependencies: + color-name: 2.1.0 + dev: false + + /color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + dev: false + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + + /component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + + /cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + dev: false + + /cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + dev: false + + /cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + dev: true + + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + dev: false + + /create-jest@29.7.0(@types/node@20.19.27): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.19.27) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + /damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + dev: true + + /data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + /data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + /data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + + /debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + + /debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + + /dedent@1.7.1: + resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + dev: true + + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + /denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + dev: false + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + + /detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + dev: true + + /dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + dev: true + + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + + /dotenv-cli@11.0.0: + resolution: {integrity: sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww==} + hasBin: true + dependencies: + cross-spawn: 7.0.6 + dotenv: 17.2.3 + dotenv-expand: 12.0.3 + minimist: 1.2.8 + dev: true + + /dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + dependencies: + dotenv: 16.6.1 + dev: true + + /dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + dev: true + + /dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + dev: true + + /dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: false + + /electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + dev: true + + /emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + + /enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + dev: false + + /encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + dev: false + + /error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + dependencies: + is-arrayish: 0.2.1 + dev: true + + /error@7.0.2: + resolution: {integrity: sha512-UtVv4l5MhijsYUxPJo4390gzfZvAnTHreNnDjnTZaKIiZ/SemXxAhBkYSKtWa5RtBXbLP8tMgn/n0RUa/H7jXw==} + dependencies: + string-template: 0.2.1 + xtend: 4.0.2 + dev: false + + /es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + /es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + /es-iterator-helpers@1.2.2: + resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + dev: true + + /es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + + /es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + /es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + dependencies: + hasown: 2.0.2 + + /es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + /esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + dev: true + + /escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + /eslint-config-next@14.2.35(eslint@8.57.1)(typescript@5.9.3): + resolution: {integrity: sha512-BpLsv01UisH193WyT/1lpHqq5iJ/Orfz9h/NOOlAmTUq4GY349PextQ62K4XpnaM9supeiEn3TaOTeQO07gURg==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@next/eslint-plugin-next': 14.2.35 + '@rushstack/eslint-patch': 1.15.0 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) + eslint-plugin-react: 7.37.5(eslint@8.57.1) + eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) + typescript: 5.9.3 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + dev: true + + /eslint-config-prettier@9.1.2(eslint@8.57.1): + resolution: {integrity: sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.57.1 + dev: false + + /eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + /eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 8.57.1 + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + get-tsconfig: 4.13.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + transitivePeerDependencies: + - supports-color + + /eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + debug: 3.2.7 + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + transitivePeerDependencies: + - supports-color + + /eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@rtsao/scc': 1.1.0 + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + /eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1): + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.11.0 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 8.57.1 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + dev: true + + /eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1): + resolution: {integrity: sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + dependencies: + eslint: 8.57.1 + dev: true + + /eslint-plugin-react@7.37.5(eslint@8.57.1): + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.2 + eslint: 8.57.1 + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + /eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + dev: true + + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + 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 + dev: true + + /express-rate-limit@7.5.1(express@4.22.1): + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + dependencies: + express: 4.22.1 + dev: false + + /express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.4 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + 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.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + dev: false + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + /fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + /fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + dev: true + + /fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + dependencies: + reusify: 1.1.0 + + /fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + dependencies: + bser: 2.1.1 + dev: true + + /fdir@6.5.0(picomatch@4.0.3): + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + dependencies: + picomatch: 4.0.3 + + /fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + dev: false + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.2.0 + + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + + /finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + 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 + transitivePeerDependencies: + - supports-color + dev: false + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + /flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + /fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + dev: false + + /follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + + /for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + + /foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + dev: true + + /form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + /formidable@2.1.5: + resolution: {integrity: sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==} + dependencies: + '@paralleldrive/cuid2': 2.3.1 + dezalgo: 1.0.4 + once: 1.4.0 + qs: 6.14.0 + dev: true + + /forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + dev: false + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + dev: true + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + /function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + /gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + /get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + 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 + + /get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + dev: true + + /get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: true + + /get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + /get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + dependencies: + resolve-pkg-maps: 1.0.0 + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.3.1 + jackspeak: 2.3.6 + minimatch: 9.0.5 + minipass: 7.1.2 + path-scurry: 1.11.1 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + /globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + + /globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + /google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + dev: false + + /gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + /handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + dev: true + + /has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.1 + + /has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + dependencies: + dunder-proto: 1.0.1 + + /has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.1.0 + + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + + /helmet@7.2.0: + resolution: {integrity: sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==} + engines: {node: '>=16.0.0'} + dev: false + + /hexer@1.5.0: + resolution: {integrity: sha512-dyrPC8KzBzUJ19QTIo1gXNqIISRXQ0NwteW6OeQHRN4ZuZeHkdODfj0zHBdOlHbRY8GqbqK57C9oWSvQZizFsg==} + engines: {node: '>= 0.10.x'} + hasBin: true + dependencies: + ansi-color: 0.2.2 + minimist: 1.2.8 + process: 0.10.1 + xtend: 4.0.2 + dev: false + + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true + + /http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + dev: false + + /https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + dev: false + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: true + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + /import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + /import-in-the-middle@1.15.0: + resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + dev: false + + /import-in-the-middle@2.0.1: + resolution: {integrity: sha512-bruMpJ7xz+9jwGzrwEhWgvRrlKRYCRDBrfU+ur3FcasYXLJDxTruJ//8g2Noj+QFyRBeqbpj8Bhn4Fbw6HjvhA==} + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + dev: false + + /import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + /ioredis@5.8.2: + resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} + engines: {node: '>=12.22.0'} + dependencies: + '@ioredis/commands': 1.4.0 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + + /is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true + + /is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + /is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + dependencies: + has-bigints: 1.1.0 + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.3.0 + dev: true + + /is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + /is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + dependencies: + semver: 7.7.3 + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + /is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + dependencies: + hasown: 2.0.2 + + /is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + /is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + /is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + /is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + dev: true + + /is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + + /is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + /is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + /is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + /is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + /is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + /is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + /is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + /is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + /is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.19 + + /is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + /is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + + /is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true + + /iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + dev: true + + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + + /jaeger-client@3.19.0: + resolution: {integrity: sha512-M0c7cKHmdyEUtjemnJyx/y9uX16XHocL46yQvyqDlPdvAcwPDbHrIbKjQdBqtiE4apQ/9dmr+ZLJYYPGnurgpw==} + engines: {node: '>=10'} + dependencies: + node-int64: 0.4.0 + opentracing: 0.14.7 + thriftrw: 3.11.4 + uuid: 8.3.2 + xorshift: 1.2.0 + dev: false + + /jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + dev: true + + /jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.27 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.1 + is-generator-fn: 2.1.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.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + + /jest-cli@29.7.0(@types/node@20.19.27): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.19.27) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.19.27) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /jest-config@29.7.0(@types/node@20.19.27): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + 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 + dependencies: + '@babel/core': 7.28.5 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.27 + babel-jest: 29.7.0(@babel/core@7.28.5) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + 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.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + detect-newline: 3.1.0 + dev: true + + /jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + dev: true + + /jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.27 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 20.19.27 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: true + + /jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.19.27 + jest-util: 29.7.0 + dev: true + + /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.7.0 + dev: true + + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.11 + resolve.exports: 2.0.3 + slash: 3.0.0 + dev: true + + /jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + 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': 20.19.27 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + 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 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + 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': 20.19.27 + chalk: 4.1.2 + cjs-module-lexer: 1.4.3 + collect-v8-coverage: 1.0.3 + glob: 7.2.3 + graceful-fs: 4.2.11 + 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 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.28.5 + '@babel/generator': 7.28.5 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) + '@babel/types': 7.28.5 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + 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.7.3 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.19.27 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + dev: true + + /jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + dev: true + + /jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.27 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + dev: true + + /jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 20.19.27 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + + /jest@29.7.0(@types/node@20.19.27): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@20.19.27) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + /js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: true + + /js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + dependencies: + argparse: 2.0.1 + + /jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + dependencies: + bignumber.js: 9.3.1 + dev: false + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + dependencies: + minimist: 1.2.8 + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + 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.1.1 + ms: 2.1.3 + semver: 7.7.3 + dev: false + + /jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + dev: true + + /jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + dev: false + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + dev: true + + /kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + dev: false + + /language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + dev: true + + /language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + dependencies: + language-subtag-registry: 0.3.23 + dev: true + + /leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + /lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + + /lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + dev: false + + /lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + dev: false + + /lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + dev: false + + /lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + dev: false + + /lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + dev: false + + /lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + dev: false + + /lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + dev: false + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: false + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false + + /lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + /lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: false + + /logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + dev: false + + /long@2.4.0: + resolution: {integrity: sha512-ijUtjmO/n2A5PaosNG9ZGDsQ3vxJg7ZW8vsY8Kp0f2yIZWhSJvjmegV7t+9RPQKxKrvj8yKGehhS+po14hPLGQ==} + engines: {node: '>=0.6'} + dev: false + + /long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + dev: false + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + + /lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + dev: true + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.7.3 + dev: true + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + + /makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + dependencies: + tmpl: 1.0.5 + dev: true + + /math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + + /merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + dev: false + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + /micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + dev: true + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.12 + + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.2 + + /minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.2 + dev: true + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + /minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + + /module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + dev: false + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + + /nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + /napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: true + + /next@14.2.35(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + sass: + optional: true + dependencies: + '@next/env': 14.2.35 + '@swc/helpers': 0.5.5 + busboy: 1.6.0 + caniuse-lite: 1.0.30001761 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.1(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 14.2.33 + '@next/swc-darwin-x64': 14.2.33 + '@next/swc-linux-arm64-gnu': 14.2.33 + '@next/swc-linux-arm64-musl': 14.2.33 + '@next/swc-linux-x64-gnu': 14.2.33 + '@next/swc-linux-x64-musl': 14.2.33 + '@next/swc-win32-arm64-msvc': 14.2.33 + '@next/swc-win32-ia32-msvc': 14.2.33 + '@next/swc-win32-x64-msvc': 14.2.33 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + + /node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + /node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + + /object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + /object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + /object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + dev: true + + /object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + + /object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + /object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + + /one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + dependencies: + fn.name: 1.1.0 + dev: false + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: true + + /opentracing@0.14.7: + resolution: {integrity: sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q==} + engines: {node: '>=0.10'} + dev: false + + /optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + /own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: true + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + /path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + dev: true + + /path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + dev: false + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + /pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + dev: false + + /pg-protocol@1.10.3: + resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + dev: false + + /pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + dev: false + + /picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + /picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + dev: true + + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + dev: true + + /possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + /postcss-import@15.1.0(postcss@8.5.6): + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + dev: true + + /postcss-js@4.1.0(postcss@8.5.6): + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + dev: true + + /postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6): + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + dependencies: + jiti: 1.21.7 + lilconfig: 3.1.3 + postcss: 8.5.6 + dev: true + + /postcss-nested@6.2.0(postcss@8.5.6): + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + dev: true + + /postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + dev: false + + /postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + dev: true + + /postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + dev: false + + /postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + dependencies: + xtend: 4.0.2 + dev: false + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + /prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + engines: {node: '>=14'} + hasBin: true + + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + dev: true + + /prisma@5.22.0: + resolution: {integrity: sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==} + engines: {node: '>=16.13'} + hasBin: true + requiresBuild: true + dependencies: + '@prisma/engines': 5.22.0 + optionalDependencies: + fsevents: 2.3.3 + + /process@0.10.1: + resolution: {integrity: sha512-dyIett8dgGIZ/TXKUzeYExt7WA6ldDzys9vTDU/cCA9L17Ypme+KzS+NjQCjpn9xsvi/shbMC+yP/BcFMBz0NA==} + engines: {node: '>= 0.6.0'} + dev: false + + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + dev: true + + /prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + dev: true + + /protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + requiresBuild: true + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.19.27 + long: 5.3.2 + dev: false + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + /pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + dev: true + + /qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.1.0 + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /react-dom@18.3.1(react@18.3.1): + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + dev: false + + /react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + dev: true + + /react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + dev: true + + /react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: true + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + dev: false + + /redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + dependencies: + redis-errors: 1.2.0 + dev: false + + /reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + /regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + /require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + dev: false + + /require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + dev: false + + /resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + dev: true + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + /resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + dev: true + + /resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + /resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + dependencies: + glob: 7.2.3 + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + + /safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + /safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + /safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + /semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + /send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + dev: false + + /serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + dev: false + + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + /set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + /set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + /shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + dev: false + + /side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + /side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + /side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + /side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: true + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + /source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + /source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + + /stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + /stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + dev: false + + /stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + dev: true + + /standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + dev: false + + /statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + dev: false + + /stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + /streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: false + + /string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + dev: true + + /string-template@0.2.1: + resolution: {integrity: sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==} + dev: false + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + dev: true + + /string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + dev: true + + /string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + dev: true + + /string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.1 + dev: true + + /string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + /string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + /string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + + /strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.2.2 + dev: true + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + /strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + dev: true + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + /styled-jsx@5.1.1(react@18.3.1): + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + dependencies: + client-only: 0.0.1 + react: 18.3.1 + dev: false + + /sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + dev: true + + /superagent@8.1.2: + resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} + engines: {node: '>=6.4.0 <13 || >=14'} + deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.3 + fast-safe-stringify: 2.1.1 + form-data: 4.0.5 + formidable: 2.1.5 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.14.0 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + dev: true + + /supertest@6.3.4: + resolution: {integrity: sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==} + engines: {node: '>=6.4.0'} + deprecated: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net + dependencies: + methods: 1.1.2 + superagent: 8.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + /tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + dev: true + + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + + /text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + dev: false + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: true + + /thriftrw@3.11.4: + resolution: {integrity: sha512-UcuBd3eanB3T10nXWRRMwfwoaC6VMk7qe3/5YIWP2Jtw+EbHqJ0p1/K3x8ixiR5dozKSSfcg1W+0e33G1Di3XA==} + engines: {node: '>= 0.10.x'} + hasBin: true + dependencies: + bufrw: 1.4.0 + error: 7.0.2 + long: 2.4.0 + dev: false + + /tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + /tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + + /triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + dev: false + + /ts-api-utils@1.4.3(typescript@5.9.3): + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.9.3 + + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + + /ts-jest@29.4.6(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.9.3): + resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + 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 + esbuild: '*' + 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 + dependencies: + '@babel/core': 7.28.5 + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@20.19.27) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.3 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + dev: true + + /tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + /tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + /tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + esbuild: 0.27.2 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /turbo-darwin-64@1.13.4: + resolution: {integrity: sha512-A0eKd73R7CGnRinTiS7txkMElg+R5rKFp9HV7baDiEL4xTG1FIg/56Vm7A5RVgg8UNgG2qNnrfatJtb+dRmNdw==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /turbo-darwin-arm64@1.13.4: + resolution: {integrity: sha512-eG769Q0NF6/Vyjsr3mKCnkG/eW6dKMBZk6dxWOdrHfrg6QgfkBUk0WUUujzdtVPiUIvsh4l46vQrNVd9EOtbyA==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /turbo-linux-64@1.13.4: + resolution: {integrity: sha512-Bq0JphDeNw3XEi+Xb/e4xoKhs1DHN7OoLVUbTIQz+gazYjigVZvtwCvgrZI7eW9Xo1eOXM2zw2u1DGLLUfmGkQ==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /turbo-linux-arm64@1.13.4: + resolution: {integrity: sha512-BJcXw1DDiHO/okYbaNdcWN6szjXyHWx9d460v6fCHY65G8CyqGU3y2uUTPK89o8lq/b2C8NK0yZD+Vp0f9VoIg==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /turbo-windows-64@1.13.4: + resolution: {integrity: sha512-OFFhXHOFLN7A78vD/dlVuuSSVEB3s9ZBj18Tm1hk3aW1HTWTuAw0ReN6ZNlVObZUHvGy8d57OAGGxf2bT3etQw==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /turbo-windows-arm64@1.13.4: + resolution: {integrity: sha512-u5A+VOKHswJJmJ8o8rcilBfU5U3Y1TTAfP9wX8bFh8teYF1ghP0EhtMRLjhtp6RPa+XCxHHVA2CiC3gbh5eg5g==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /turbo@1.13.4: + resolution: {integrity: sha512-1q7+9UJABuBAHrcC4Sxp5lOqYS5mvxRrwa33wpIyM18hlOCpRD/fTJNxZ0vhbMcJmz15o9kkVm743mPn7p6jpQ==} + hasBin: true + optionalDependencies: + turbo-darwin-64: 1.13.4 + turbo-darwin-arm64: 1.13.4 + turbo-linux-64: 1.13.4 + turbo-linux-arm64: 1.13.4 + turbo-windows-64: 1.13.4 + turbo-windows-arm64: 1.13.4 + dev: true + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + dev: true + + /type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + dev: true + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false + + /typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + /typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + /typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + /typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + /typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + /uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + requiresBuild: true + dev: true + optional: true + + /unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + /undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + + /unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + requiresBuild: true + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + /update-browserslist-db@1.2.3(browserslist@4.28.1): + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.1 + + /use-sync-external-store@1.6.0(react@18.3.1): + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + dependencies: + react: 18.3.1 + dev: false + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: false + + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: false + + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false + + /v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + dev: true + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false + + /walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + dependencies: + makeerror: 1.0.12 + dev: true + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + + /which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + /which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + /which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + /which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + + /winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + dev: false + + /winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.8 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + dev: false + + /word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + /wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + /write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + dev: true + + /xorshift@1.2.0: + resolution: {integrity: sha512-iYgNnGyeeJ4t6U11NpA/QiKy+PXn5Aa3Azg5qkwIFz1tBLllQrjjsk9yzD7IAK0naNU4JxdeDgqW9ov4u/hc4g==} + dev: false + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: false + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + /zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + dev: false + + /zustand@4.5.7(@types/react@18.3.27)(react@18.3.1): + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + dependencies: + '@types/react': 18.3.27 + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) + dev: false diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..45f8b794 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +packages: + - 'apps/*' + - 'services/*' + - 'packages/*' + - 'packages/config/*' diff --git a/scripts/db/backup.sh b/scripts/db/backup.sh new file mode 100755 index 00000000..d091f6dc --- /dev/null +++ b/scripts/db/backup.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +set -e + +SERVICE=$1 +BACKUP_DIR="${2:-./backups}" + +if [ -z "$SERVICE" ]; then + echo "Usage: $0 [backup-dir]" + echo "Example: $0 auth-service" + exit 1 +fi + +mkdir -p "$BACKUP_DIR" + +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/${SERVICE}_${TIMESTAMP}.sql" + +echo "💾 Backing up database for $SERVICE..." + +# Extract database URL from .env or environment +cd "services/$SERVICE" + +if [ -z "$DATABASE_URL" ]; then + if [ -f ".env" ]; then + DATABASE_URL=$(grep DATABASE_URL .env | cut -d '=' -f2- | tr -d '"' | tr -d "'") + elif [ -f "../../deployments/local/.env.local" ]; then + DATABASE_URL=$(grep DATABASE_URL ../../deployments/local/.env.local | cut -d '=' -f2- | tr -d '"' | tr -d "'") + fi +fi + +if [ -z "$DATABASE_URL" ]; then + echo "❌ DATABASE_URL not found. Please set it in:" + echo " - services/$SERVICE/.env" + echo " - deployments/local/.env.local" + echo " - Or as environment variable" + exit 1 +fi + +# Use pg_dump directly with DATABASE_URL (works with Neon and standard PostgreSQL) +# Neon URLs format: postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require +# Standard format: postgresql://user:pass@host:port/dbname + +# Check if pg_dump supports connection string (PostgreSQL 11+) +if pg_dump --version | grep -qE "1[1-9]|2[0-9]"; then + # Use connection string directly (supports Neon) + pg_dump "$DATABASE_URL" > "../$BACKUP_FILE" +else + # Fallback: Parse connection string for older pg_dump + # Remove query parameters for parsing + CLEAN_URL=$(echo $DATABASE_URL | sed 's/?.*//') + DB_USER=$(echo $CLEAN_URL | sed -n 's/.*:\/\/\([^:]*\):.*/\1/p') + DB_PASS=$(echo $CLEAN_URL | sed -n 's/.*:\/\/[^:]*:\([^@]*\)@.*/\1/p') + DB_HOST=$(echo $CLEAN_URL | sed -n 's/.*@\([^:]*\):.*/\1/p') + DB_PORT=$(echo $CLEAN_URL | sed -n 's/.*:\([0-9]*\)\/.*/\1/p') + DB_NAME=$(echo $CLEAN_URL | sed -n 's/.*\/\([^?]*\).*/\1/p') + + PGPASSWORD=$DB_PASS pg_dump -h $DB_HOST -p ${DB_PORT:-5432} -U $DB_USER -d $DB_NAME > "../$BACKUP_FILE" +fi + +echo "✅ Backup created: $BACKUP_FILE" diff --git a/scripts/db/migrate.sh b/scripts/db/migrate.sh new file mode 100755 index 00000000..5929b339 --- /dev/null +++ b/scripts/db/migrate.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +set -e + +SERVICE=$1 + +if [ -z "$SERVICE" ]; then + echo "Usage: $0 [dev|deploy]" + echo "Example: $0 auth-service dev" + echo "Example: $0 auth-service deploy" + exit 1 +fi + +if [ ! -d "services/$SERVICE" ]; then + echo "❌ Service $SERVICE not found" + exit 1 +fi + +echo "🔄 Running migrations for $SERVICE..." + +cd "services/$SERVICE" + +# Load environment variables (hybrid pattern) +# 1. Load shared env (JWT secrets, Redis config) +if [ -f "../../deployments/local/.env.local" ]; then + export $(grep -v '^#' ../../deployments/local/.env.local | xargs) +fi + +# 2. Load service-specific env (DATABASE_URL, PORT, etc.) +if [ -f ".env.local" ]; then + export $(grep -v '^#' .env.local | xargs) +fi + +# Check if DATABASE_URL is set +if [ -z "$DATABASE_URL" ]; then + echo "⚠️ DATABASE_URL not set. Please check your .env.local files:" + echo " - Shared config: ../../deployments/local/.env.local" + echo " - Service config: .env.local" + echo " For Neon: postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true" + exit 1 +fi + +if [ "$2" = "dev" ]; then + echo "📝 Running development migration (creates new migration)..." + pnpm prisma migrate dev +else + echo "🚀 Running production migration (applies existing migrations)..." + pnpm prisma migrate deploy +fi + +echo "✅ Migrations completed!" diff --git a/scripts/db/seed.sh b/scripts/db/seed.sh new file mode 100755 index 00000000..352f96c5 --- /dev/null +++ b/scripts/db/seed.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +set -e + +SERVICE=$1 + +if [ -z "$SERVICE" ]; then + echo "Usage: $0 " + echo "Example: $0 auth-service" + exit 1 +fi + +if [ ! -d "services/$SERVICE" ]; then + echo "❌ Service $SERVICE not found" + exit 1 +fi + +echo "🌱 Seeding database for $SERVICE..." + +cd "services/$SERVICE" + +# Check if DATABASE_URL is set +if [ -z "$DATABASE_URL" ]; then + if [ -f ".env" ]; then + export $(grep -v '^#' .env | xargs) + elif [ -f "../../deployments/local/.env.local" ]; then + export $(grep -v '^#' ../../deployments/local/.env.local | xargs) + else + echo "⚠️ DATABASE_URL not set. Please set it in .env or environment variable." + exit 1 + fi +fi + +pnpm prisma:seed + +echo "✅ Database seeded!" diff --git a/scripts/db/setup-neon.sh b/scripts/db/setup-neon.sh new file mode 100755 index 00000000..a646f97d --- /dev/null +++ b/scripts/db/setup-neon.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +set -e + +echo "🚀 Neon Database Setup Script" +echo "" + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${YELLOW}This script helps you set up Neon database for all environments.${NC}" +echo "" + +# Check if .env.local exists +ENV_LOCAL="deployments/local/.env.local" +if [ ! -f "$ENV_LOCAL" ]; then + echo "📝 Creating $ENV_LOCAL from example..." + cp deployments/local/env.local.example "$ENV_LOCAL" + echo -e "${GREEN}✅ Created $ENV_LOCAL${NC}" + echo "" +fi + +echo "📋 Setup Steps:" +echo "" +echo "1. Go to https://console.neon.tech" +echo "2. Create a project (or use existing): goodgo-platform" +echo "3. Create branches:" +echo " - staging (from main)" +echo " - production (from main)" +echo "" +echo "4. Get connection strings for each branch from Neon Console" +echo " Format: postgresql://user:password@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true" +echo "" + +read -p "Enter development DATABASE_URL (main branch): " dev_url +if [ -n "$dev_url" ]; then + # Update .env.local + if grep -q "DATABASE_URL=" "$ENV_LOCAL"; then + sed -i.bak "s|DATABASE_URL=.*|DATABASE_URL=$dev_url|" "$ENV_LOCAL" + rm -f "${ENV_LOCAL}.bak" + else + echo "DATABASE_URL=$dev_url" >> "$ENV_LOCAL" + fi + echo -e "${GREEN}✅ Updated $ENV_LOCAL${NC}" +fi + +echo "" +echo "📝 Next Steps:" +echo "" +echo "1. Add to GitHub Secrets (for CI/CD):" +echo " - NEON_DATABASE_URL_STAGING" +echo " - NEON_DATABASE_URL_PRODUCTION" +echo " - NEON_DATABASE_URL_TEST (optional, for CI tests)" +echo "" +echo "2. Create Kubernetes secrets for staging/production:" +echo " See: deployments/staging/kubernetes/secrets.yaml.example" +echo " See: deployments/production/kubernetes/secrets.yaml.example" +echo "" +echo "3. Run initial migration:" +echo " ./scripts/db/migrate.sh auth-service dev" +echo "" +echo "4. Seed database (optional):" +echo " ./scripts/db/seed.sh auth-service" +echo "" +echo -e "${GREEN}✅ Setup instructions complete!${NC}" +echo "" +echo "For more details, see: infra/databases/neon/README.md" diff --git a/scripts/deploy/deploy-prod.sh b/scripts/deploy/deploy-prod.sh new file mode 100755 index 00000000..123d8c5a --- /dev/null +++ b/scripts/deploy/deploy-prod.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e + +echo "⚠️ WARNING: You are about to deploy to PRODUCTION!" +read -p "Are you sure? (yes/no): " confirm + +if [ "$confirm" != "yes" ]; then + echo "Deployment cancelled" + exit 1 +fi + +echo "🚀 Deploying to production..." + +if [ -z "$KUBECONFIG" ]; then + echo "❌ KUBECONFIG environment variable not set" + exit 1 +fi + +kubectl apply -f deployments/production/kubernetes/ +kubectl rollout status deployment/auth-service -n production + +echo "✅ Deployment completed!" diff --git a/scripts/deploy/deploy-staging.sh b/scripts/deploy/deploy-staging.sh new file mode 100755 index 00000000..5022a375 --- /dev/null +++ b/scripts/deploy/deploy-staging.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -e + +echo "🚀 Deploying to staging..." + +if [ -z "$KUBECONFIG" ]; then + echo "❌ KUBECONFIG environment variable not set" + exit 1 +fi + +kubectl apply -f deployments/staging/kubernetes/ +kubectl rollout status deployment/auth-service -n staging + +echo "✅ Deployment completed!" diff --git a/scripts/dev/logs.sh b/scripts/dev/logs.sh new file mode 100755 index 00000000..6326e107 --- /dev/null +++ b/scripts/dev/logs.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +SERVICE=$1 + +if [ -z "$SERVICE" ]; then + echo "Usage: $0 " + echo "Example: $0 auth-service" + echo "" + echo "Or use 'docker' to view Docker logs:" + echo " $0 docker " + exit 1 +fi + +if [ "$SERVICE" = "docker" ]; then + CONTAINER=$2 + if [ -z "$CONTAINER" ]; then + echo "Usage: $0 docker " + echo "Available containers:" + docker ps --format "{{.Names}}" + exit 1 + fi + docker logs -f "$CONTAINER" +else + cd "services/$SERVICE" + pnpm dev +fi diff --git a/scripts/dev/setup-env.sh b/scripts/dev/setup-env.sh new file mode 100755 index 00000000..f1bd9bfd --- /dev/null +++ b/scripts/dev/setup-env.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +# Setup Environment Variables for Development +# Thiết lập Environment Variables cho Development + +set -e + +echo "🔧 Setting up environment variables..." + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check if we're in the project root +if [ ! -f "package.json" ]; then + echo -e "${RED}❌ Error: Must run from project root${NC}" + exit 1 +fi + +# Setup shared environment +echo "" +echo "📦 Step 1: Setup shared environment (deployments/local/.env.local)" +if [ -f "deployments/local/.env.local" ]; then + echo -e "${YELLOW}⚠️ deployments/local/.env.local already exists${NC}" + read -p "Overwrite? (y/n): " overwrite + if [ "$overwrite" != "y" ]; then + echo "Skipping shared environment setup" + else + cp deployments/local/env.local.example deployments/local/.env.local + echo -e "${GREEN}✅ Created deployments/local/.env.local${NC}" + fi +else + cp deployments/local/env.local.example deployments/local/.env.local + echo -e "${GREEN}✅ Created deployments/local/.env.local${NC}" +fi + +# Setup service-specific environments +echo "" +echo "📦 Step 2: Setup service-specific environments" + +# Function to setup service env +setup_service_env() { + local service=$1 + local service_path="services/$service" + + if [ ! -d "$service_path" ]; then + echo -e "${YELLOW}⚠️ Service $service not found, skipping${NC}" + return + fi + + if [ -f "$service_path/.env.local" ]; then + echo -e "${YELLOW}⚠️ $service_path/.env.local already exists, skipping${NC}" + else + if [ -f "$service_path/env.local.example" ]; then + cp "$service_path/env.local.example" "$service_path/.env.local" + echo -e "${GREEN}✅ Created $service_path/.env.local${NC}" + else + echo -e "${YELLOW}⚠️ $service_path/env.local.example not found, skipping${NC}" + fi + fi +} + +# Setup for all services +for service_dir in services/*/; do + if [ -d "$service_dir" ]; then + service_name=$(basename "$service_dir") + if [ "$service_name" != "_template" ]; then + setup_service_env "$service_name" + fi + fi +done + +# Summary +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo -e "${GREEN}✅ Environment setup complete!${NC}" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" +echo "📝 Next steps:" +echo "" +echo "1. Edit deployments/local/.env.local:" +echo " - Set JWT_SECRET and JWT_REFRESH_SECRET" +echo "" +echo "2. Edit services//.env.local for each service:" +echo " - Set DATABASE_URL from Neon Console" +echo " - Each service needs its own database" +echo "" +echo "3. Create databases in Neon:" +echo " - goodgo_auth_dev (for auth-service)" +echo " - goodgo_user_dev (for user-service)" +echo " - etc." +echo "" +echo "4. Run migrations:" +echo " ./scripts/db/migrate.sh auth-service dev" +echo "" +echo "5. Start development:" +echo " ./scripts/dev/start-all.sh" +echo "" +echo "📚 See docs/vi/guides/local-development.md for details" +echo "" diff --git a/scripts/dev/start-all.sh b/scripts/dev/start-all.sh new file mode 100755 index 00000000..06731c26 --- /dev/null +++ b/scripts/dev/start-all.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +set -e + +echo "🚀 Starting all services..." + +# Check if Docker is running +if ! docker info &> /dev/null; then + echo "❌ Docker is not running. Please start Docker first." + exit 1 +fi + +# Check if Neon DATABASE_URL is set +if [ -f "deployments/local/.env.local" ]; then + export $(grep -v '^#' deployments/local/.env.local | xargs) +fi + +if [ -z "$DATABASE_URL" ]; then + echo "⚠️ WARNING: DATABASE_URL not set!" + echo " Please setup Neon database: ./scripts/db/setup-neon.sh" + echo " Or set DATABASE_URL in deployments/local/.env.local" + echo "" + read -p "Continue anyway? (y/n): " continue + if [ "$continue" != "y" ]; then + exit 1 + fi +fi + +# Start infrastructure (Redis, Traefik - no PostgreSQL needed) +echo "📦 Starting infrastructure (Redis, Traefik)..." +cd deployments/local +docker-compose up -d +cd ../.. + +# Wait for Redis to be ready +echo "⏳ Waiting for Redis to be ready..." +sleep 3 + +# Start services +echo "🚀 Starting services..." +pnpm dev diff --git a/scripts/dev/start-service.sh b/scripts/dev/start-service.sh new file mode 100755 index 00000000..d13c90f9 --- /dev/null +++ b/scripts/dev/start-service.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -e + +SERVICE=$1 + +if [ -z "$SERVICE" ]; then + echo "Usage: $0 " + echo "Example: $0 auth-service" + exit 1 +fi + +echo "🚀 Starting $SERVICE..." + +# Check if service exists +if [ ! -d "services/$SERVICE" ]; then + echo "❌ Service $SERVICE not found" + exit 1 +fi + +cd "services/$SERVICE" +pnpm dev diff --git a/scripts/setup/init-project.sh b/scripts/setup/init-project.sh new file mode 100755 index 00000000..03fb7a75 --- /dev/null +++ b/scripts/setup/init-project.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +set -e + +echo "🚀 Initializing GoodGo Microservices Project..." + +# Check prerequisites +if ! command -v pnpm &> /dev/null; then + echo "❌ PNPM is not installed. Please install it first: npm install -g pnpm" + exit 1 +fi + +if ! command -v docker &> /dev/null; then + echo "⚠️ Docker is not installed. Some features may not work." +fi + +# Install dependencies +echo "📦 Installing dependencies..." +pnpm install + +# Generate Prisma clients +echo "🔧 Generating Prisma clients..." +cd services/auth-service +pnpm prisma:generate || echo "⚠️ Prisma generation skipped (database not available)" +cd ../.. + +# Setup environment files +echo "📝 Setting up environment files..." +if [ ! -f "services/auth-service/.env" ]; then + cp services/auth-service/env.example services/auth-service/.env + echo "✅ Created services/auth-service/.env" +fi + +if [ ! -f "deployments/local/.env.local" ]; then + cp deployments/local/env.local.example deployments/local/.env.local + echo "✅ Created deployments/local/.env.local" + echo "⚠️ IMPORTANT: Edit .env.local and add your Neon DATABASE_URL" +fi + +if [ ! -f "apps/web-admin/.env.local" ]; then + cp apps/web-admin/.env.example apps/web-admin/.env.local 2>/dev/null || echo "⚠️ Web admin .env.example not found" +fi + +if [ ! -f "apps/web-client/.env.local" ]; then + cp apps/web-client/.env.example apps/web-client/.env.local 2>/dev/null || echo "⚠️ Web client .env.example not found" +fi + +echo "✅ Project initialization complete!" +echo "" +echo "Next steps:" +echo "1. Setup Neon database: ./scripts/db/setup-neon.sh" +echo "2. Update .env files with your Neon DATABASE_URL" +echo "3. Start infrastructure: cd deployments/local && docker-compose up -d" +echo "4. Run migrations: ./scripts/db/migrate.sh auth-service dev" +echo "5. Seed database: ./scripts/db/seed.sh auth-service" +echo "6. Start services: pnpm dev" +echo "" +echo "📚 See infra/databases/neon/README.md for Neon setup details" diff --git a/scripts/setup/install-deps.sh b/scripts/setup/install-deps.sh new file mode 100755 index 00000000..777e12ff --- /dev/null +++ b/scripts/setup/install-deps.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +echo "📦 Installing all dependencies..." + +pnpm install + +echo "✅ Dependencies installed!" diff --git a/scripts/utils/cleanup.sh b/scripts/utils/cleanup.sh new file mode 100755 index 00000000..eec9e2a6 --- /dev/null +++ b/scripts/utils/cleanup.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e + +echo "🧹 Cleaning up temporary files..." + +# Remove node_modules (optional, comment out if you want to keep them) +# find . -name "node_modules" -type d -prune -exec rm -rf {} \; + +# Remove build artifacts +find . -name "dist" -type d -prune -exec rm -rf {} \; +find . -name ".next" -type d -prune -exec rm -rf {} \; +find . -name "build" -type d -prune -exec rm -rf {} \; +find . -name "*.tsbuildinfo" -type f -delete + +# Remove logs +find . -name "*.log" -type f -delete + +# Remove cache +find . -name ".turbo" -type d -prune -exec rm -rf {} \; +find . -name ".cache" -type d -prune -exec rm -rf {} \; + +echo "✅ Cleanup completed!" diff --git a/scripts/utils/create-service.sh b/scripts/utils/create-service.sh new file mode 100755 index 00000000..f03a3ab8 --- /dev/null +++ b/scripts/utils/create-service.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +set -e + +SERVICE_NAME=$1 + +if [ -z "$SERVICE_NAME" ]; then + echo "Usage: $0 " + echo "Example: $0 payment-service" + exit 1 +fi + +SERVICE_DIR="services/$SERVICE_NAME" + +if [ -d "$SERVICE_DIR" ]; then + echo "❌ Service $SERVICE_NAME already exists" + exit 1 +fi + +echo "📦 Creating new service: $SERVICE_NAME..." + +# Copy template +cp -r services/_template "$SERVICE_DIR" + +# Update package.json +cd "$SERVICE_DIR" +sed -i.bak "s/@goodgo\/service-template/@goodgo\/$SERVICE_NAME/g" package.json +rm package.json.bak + +# Update .env.example +sed -i.bak "s/SERVICE_NAME=service-name/SERVICE_NAME=$SERVICE_NAME/g" .env.example 2>/dev/null || true +rm .env.example.bak 2>/dev/null || true + +# Update Dockerfile port if needed +# (Keep default 5000, user can change later) + +cd ../.. + +echo "✅ Service $SERVICE_NAME created!" +echo "" +echo "Next steps:" +echo "1. Update $SERVICE_DIR/package.json if needed" +echo "2. Update $SERVICE_DIR/.env.example with service-specific configs" +echo "3. Implement your service logic in $SERVICE_DIR/src/" +echo "4. Add Prisma schema if needed: $SERVICE_DIR/prisma/schema.prisma" diff --git a/services/_template/Dockerfile b/services/_template/Dockerfile new file mode 100644 index 00000000..6f342ecc --- /dev/null +++ b/services/_template/Dockerfile @@ -0,0 +1,32 @@ +FROM node:20-alpine AS base +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Dependencies stage +FROM base AS deps +RUN corepack enable pnpm +COPY package.json pnpm-lock.yaml* ./ +RUN pnpm install --frozen-lockfile + +# Builder stage +FROM base AS builder +RUN corepack enable pnpm +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN pnpm prisma generate +RUN pnpm build + +# Production stage +FROM base AS runner +ENV NODE_ENV=production +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 microservice +USER microservice + +COPY --from=builder --chown=microservice:nodejs /app/dist ./dist +COPY --from=builder --chown=microservice:nodejs /app/node_modules ./node_modules +COPY --from=builder --chown=microservice:nodejs /app/package.json ./ +COPY --from=builder --chown=microservice:nodejs /app/prisma ./prisma + +EXPOSE 5000 +CMD ["node", "dist/main.js"] diff --git a/services/_template/README.md b/services/_template/README.md new file mode 100644 index 00000000..edf318b4 --- /dev/null +++ b/services/_template/README.md @@ -0,0 +1,38 @@ +# Service Template + +Template for creating new microservices in the monorepo. + +## Usage + +1. Copy this template to create a new service: + ```bash + cp -r services/_template services/new-service + ``` + +2. Update `package.json`: + - Change `name` to `@goodgo/new-service` + - Update `description` + +3. Update `.env.example`: + - Set appropriate `PORT` + - Set `SERVICE_NAME` + - Configure database URL + +4. Implement your features: + - Add modules in `src/modules/` + - Update routes in `src/routes/index.ts` + - Add Prisma schema if needed + +5. Update Dockerfile: + - Change `EXPOSE` port if different + +6. Remove this README and create your own + +## Structure + +- `src/modules/` - Feature modules (controller, service, dto, module) +- `src/config/` - Configuration files +- `src/middlewares/` - Express middlewares +- `src/routes/` - Route definitions +- `prisma/` - Database schema and migrations +- `tests/` - Unit and integration tests diff --git a/services/_template/package.json b/services/_template/package.json new file mode 100644 index 00000000..ae33a96f --- /dev/null +++ b/services/_template/package.json @@ -0,0 +1,46 @@ +{ + "name": "@goodgo/service-template", + "version": "1.0.0", + "description": "Template for creating new microservices", + "main": "./dist/main.js", + "scripts": { + "dev": "tsx watch src/main.ts", + "build": "tsc", + "start": "node dist/main.js", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint src --ext .ts", + "typecheck": "tsc --noEmit", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:deploy": "prisma migrate deploy", + "prisma:seed": "tsx prisma/seed.ts", + "clean": "rm -rf dist" + }, + "dependencies": { + "@goodgo/logger": "workspace:*", + "@goodgo/types": "workspace:*", + "@goodgo/auth-sdk": "workspace:*", + "@goodgo/tracing": "workspace:*", + "@prisma/client": "^5.9.1", + "express": "^4.18.2", + "zod": "^3.22.4", + "cors": "^2.8.5", + "helmet": "^7.1.0", + "express-rate-limit": "^7.1.5" + }, + "devDependencies": { + "@goodgo/eslint-config": "workspace:*", + "@goodgo/tsconfig": "workspace:*", + "@types/express": "^4.17.21", + "@types/cors": "^2.8.17", + "@types/node": "^20.11.0", + "@types/jest": "^29.5.11", + "typescript": "^5.3.3", + "tsx": "^4.7.1", + "prisma": "^5.9.1", + "jest": "^29.7.0", + "ts-jest": "^29.1.2" + } +} diff --git a/services/_template/src/config/app.config.ts b/services/_template/src/config/app.config.ts new file mode 100644 index 00000000..f2cd3a12 --- /dev/null +++ b/services/_template/src/config/app.config.ts @@ -0,0 +1,6 @@ +export const appConfig = { + port: parseInt(process.env.PORT || '5000', 10), + nodeEnv: process.env.NODE_ENV || 'development', + apiVersion: process.env.API_VERSION || 'v1', + corsOrigin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:3000'], +}; diff --git a/services/_template/src/config/database.config.ts b/services/_template/src/config/database.config.ts new file mode 100644 index 00000000..6c5d23ee --- /dev/null +++ b/services/_template/src/config/database.config.ts @@ -0,0 +1,21 @@ +import { PrismaClient } from '@prisma/client'; +import { logger } from '@goodgo/logger'; + +export const prisma = new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], +}); + +export const connectDatabase = async (): Promise => { + try { + await prisma.$connect(); + logger.info('Database connected successfully'); + } catch (error) { + logger.error('Database connection failed', { error }); + process.exit(1); + } +}; + +export const disconnectDatabase = async (): Promise => { + await prisma.$disconnect(); + logger.info('Database disconnected'); +}; diff --git a/services/_template/src/main.ts b/services/_template/src/main.ts new file mode 100644 index 00000000..071c177d --- /dev/null +++ b/services/_template/src/main.ts @@ -0,0 +1,81 @@ +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import rateLimit from 'express-rate-limit'; +import { connectDatabase } from './config/database.config'; +import { appConfig } from './config/app.config'; +import { createRouter } from './routes'; +import { requestLogger } from './middlewares/logger.middleware'; +import { errorHandler, notFoundHandler } from './middlewares/error.middleware'; +import { logger } from '@goodgo/logger'; +import { initTracing } from '@goodgo/tracing'; + +// Initialize tracing +if (process.env.TRACING_ENABLED === 'true') { + initTracing({ + serviceName: process.env.SERVICE_NAME || 'microservice', + jaegerEndpoint: process.env.JAEGER_ENDPOINT, + enabled: true, + }); +} + +const app = express(); + +// Security middleware +app.use(helmet()); +app.use( + cors({ + origin: appConfig.corsOrigin, + credentials: true, + }) +); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100, +}); +app.use('/api', limiter); + +// Body parsing +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// Request logging +app.use(requestLogger); + +// Routes +app.use(createRouter()); + +// Error handling +app.use(notFoundHandler); +app.use(errorHandler); + +const startServer = async () => { + try { + await connectDatabase(); + + app.listen(appConfig.port, () => { + logger.info(`Service started on port ${appConfig.port}`, { + port: appConfig.port, + nodeEnv: appConfig.nodeEnv, + }); + }); + } catch (error) { + logger.error('Failed to start server', { error }); + process.exit(1); + } +}; + +startServer(); + +// Graceful shutdown +process.on('SIGTERM', async () => { + logger.info('SIGTERM received, shutting down gracefully'); + process.exit(0); +}); + +process.on('SIGINT', async () => { + logger.info('SIGINT received, shutting down gracefully'); + process.exit(0); +}); diff --git a/services/_template/src/middlewares/error.middleware.ts b/services/_template/src/middlewares/error.middleware.ts new file mode 100644 index 00000000..af7cc382 --- /dev/null +++ b/services/_template/src/middlewares/error.middleware.ts @@ -0,0 +1,42 @@ +import { Request, Response, NextFunction } from 'express'; +import { logger } from '@goodgo/logger'; +import { ApiResponse } from '@goodgo/types'; + +export const errorHandler = ( + err: Error, + req: Request, + res: Response, + next: NextFunction +): void => { + logger.error('Error occurred', { + error: err.message, + stack: err.stack, + path: req.path, + method: req.method, + }); + + const response: ApiResponse = { + success: false, + error: { + code: 'INTERNAL_ERROR', + message: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message, + details: process.env.NODE_ENV === 'development' ? { stack: err.stack } : undefined, + }, + timestamp: new Date().toISOString(), + }; + + res.status(500).json(response); +}; + +export const notFoundHandler = (req: Request, res: Response): void => { + const response: ApiResponse = { + success: false, + error: { + code: 'NOT_FOUND', + message: `Route ${req.path} not found`, + }, + timestamp: new Date().toISOString(), + }; + + res.status(404).json(response); +}; diff --git a/services/_template/src/middlewares/logger.middleware.ts b/services/_template/src/middlewares/logger.middleware.ts new file mode 100644 index 00000000..d7dd0c20 --- /dev/null +++ b/services/_template/src/middlewares/logger.middleware.ts @@ -0,0 +1,19 @@ +import { Request, Response, NextFunction } from 'express'; +import { logger } from '@goodgo/logger'; + +export const requestLogger = (req: Request, res: Response, next: NextFunction): void => { + const start = Date.now(); + + res.on('finish', () => { + const duration = Date.now() - start; + logger.info('HTTP Request', { + method: req.method, + path: req.path, + statusCode: res.statusCode, + duration: `${duration}ms`, + ip: req.ip, + }); + }); + + next(); +}; diff --git a/services/_template/src/modules/feature/feature.controller.ts b/services/_template/src/modules/feature/feature.controller.ts new file mode 100644 index 00000000..867db25a --- /dev/null +++ b/services/_template/src/modules/feature/feature.controller.ts @@ -0,0 +1,32 @@ +import { Request, Response } from 'express'; +import { FeatureService } from './feature.service'; +import { ApiResponse } from '@goodgo/types'; + +export class FeatureController { + private featureService: FeatureService; + + constructor() { + this.featureService = new FeatureService(); + } + + create = async (req: Request, res: Response): Promise => { + try { + // Implementation + const response: ApiResponse = { + success: true, + message: 'Feature created', + timestamp: new Date().toISOString(), + }; + res.json(response); + } catch (error: any) { + res.status(400).json({ + success: false, + error: { + code: 'FEATURE_001', + message: error.message || 'Failed to create feature', + }, + timestamp: new Date().toISOString(), + }); + } + }; +} diff --git a/services/_template/src/modules/feature/feature.dto.ts b/services/_template/src/modules/feature/feature.dto.ts new file mode 100644 index 00000000..c71080e0 --- /dev/null +++ b/services/_template/src/modules/feature/feature.dto.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +// Example DTO - replace with your feature DTOs +export const createFeatureDtoSchema = z.object({ + name: z.string().min(1), + description: z.string().optional(), +}); + +export type CreateFeatureDto = z.infer; diff --git a/services/_template/src/modules/feature/feature.module.ts b/services/_template/src/modules/feature/feature.module.ts new file mode 100644 index 00000000..fa9a314e --- /dev/null +++ b/services/_template/src/modules/feature/feature.module.ts @@ -0,0 +1,11 @@ +import { Router } from 'express'; +import { FeatureController } from './feature.controller'; + +export const createFeatureRouter = (): Router => { + const router = Router(); + const featureController = new FeatureController(); + + router.post('/', featureController.create); + + return router; +}; diff --git a/services/_template/src/modules/feature/feature.service.ts b/services/_template/src/modules/feature/feature.service.ts new file mode 100644 index 00000000..b79755d6 --- /dev/null +++ b/services/_template/src/modules/feature/feature.service.ts @@ -0,0 +1,10 @@ +import { logger } from '@goodgo/logger'; +// Import your service dependencies here + +export class FeatureService { + // Implement your service methods here + async create(data: any) { + logger.info('Creating feature', { data }); + // Implementation + } +} diff --git a/services/_template/src/modules/health/health.controller.ts b/services/_template/src/modules/health/health.controller.ts new file mode 100644 index 00000000..7e874d53 --- /dev/null +++ b/services/_template/src/modules/health/health.controller.ts @@ -0,0 +1,46 @@ +import { Request, Response } from 'express'; +import { prisma } from '../../config/database.config'; +import { ApiResponse } from '@goodgo/types'; + +export class HealthController { + health = async (req: Request, res: Response): Promise => { + const response: ApiResponse<{ status: string; timestamp: string }> = { + success: true, + data: { + status: 'ok', + timestamp: new Date().toISOString(), + }, + timestamp: new Date().toISOString(), + }; + + res.json(response); + }; + + ready = async (req: Request, res: Response): Promise => { + try { + await prisma.$queryRaw`SELECT 1`; + res.json({ + success: true, + data: { status: 'ready' }, + timestamp: new Date().toISOString(), + }); + } catch (error) { + res.status(503).json({ + success: false, + error: { + code: 'HEALTH_001', + message: 'Service not ready', + }, + timestamp: new Date().toISOString(), + }); + } + }; + + live = async (req: Request, res: Response): Promise => { + res.json({ + success: true, + data: { status: 'live' }, + timestamp: new Date().toISOString(), + }); + }; +} diff --git a/services/_template/src/routes/index.ts b/services/_template/src/routes/index.ts new file mode 100644 index 00000000..72129851 --- /dev/null +++ b/services/_template/src/routes/index.ts @@ -0,0 +1,20 @@ +import { Router } from 'express'; +import { createFeatureRouter } from '../modules/feature/feature.module'; +import { HealthController } from '../modules/health/health.controller'; + +export const createRouter = (): Router => { + const router = Router(); + const healthController = new HealthController(); + + const apiVersion = process.env.API_VERSION || 'v1'; + + // Health checks + router.get('/health', healthController.health); + router.get('/health/ready', healthController.ready); + router.get('/health/live', healthController.live); + + // API routes + router.use(`/api/${apiVersion}/features`, createFeatureRouter()); + + return router; +}; diff --git a/services/_template/tsconfig.json b/services/_template/tsconfig.json new file mode 100644 index 00000000..87c2999a --- /dev/null +++ b/services/_template/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@goodgo/tsconfig/node.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/services/auth-service/Dockerfile b/services/auth-service/Dockerfile new file mode 100644 index 00000000..789f3978 --- /dev/null +++ b/services/auth-service/Dockerfile @@ -0,0 +1,51 @@ +FROM node:20-alpine AS base +RUN apk add --no-cache libc6-compat openssl +WORKDIR /app + +# Builder stage +FROM base AS builder +RUN corepack enable pnpm +# Copy workspace configuration +COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ +# Create directory structure and copy all package.json files +RUN mkdir -p packages apps services +COPY packages/auth-sdk/package.json ./packages/auth-sdk/ +COPY packages/http-client/package.json ./packages/http-client/ +COPY packages/logger/package.json ./packages/logger/ +COPY packages/tracing/package.json ./packages/tracing/ +COPY packages/types/package.json ./packages/types/ +COPY packages/config/eslint-config/package.json ./packages/config/eslint-config/ +COPY packages/config/prettier-config/package.json ./packages/config/prettier-config/ +COPY packages/config/tsconfig/package.json ./packages/config/tsconfig/ +COPY apps/web-client/package.json ./apps/web-client/ +COPY apps/web-admin/package.json ./apps/web-admin/ +COPY services/auth-service/package.json ./services/auth-service/ +# Install all dependencies for entire monorepo +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile +# Copy all source code +COPY packages ./packages +COPY services/auth-service ./services/auth-service +COPY turbo.json ./ +# Generate Prisma client +WORKDIR /app/services/auth-service +RUN pnpm prisma generate +# Build using turbo from root (handles dependency order automatically) +WORKDIR /app +RUN pnpm turbo build --filter=auth-service + +# Production stage +FROM base AS runner +ENV NODE_ENV=production +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 microservice + +# Copy the entire workspace structure to preserve pnpm's node_modules layout +COPY --from=builder --chown=microservice:nodejs /app /app + +WORKDIR /app/services/auth-service + +USER microservice + +EXPOSE 5001 +CMD ["node", "dist/main.js"] diff --git a/services/auth-service/README.md b/services/auth-service/README.md new file mode 100644 index 00000000..1fb29048 --- /dev/null +++ b/services/auth-service/README.md @@ -0,0 +1,131 @@ +# Auth Service + +Authentication and Authorization microservice. + +## Features + +- User registration and login +- JWT token management (access + refresh tokens) +- Password change +- User management (admin) +- Role-based access control (RBAC) +- Health checks + +## API Endpoints + +### Auth +- `POST /api/v1/auth/register` - Register new user +- `POST /api/v1/auth/login` - Login user +- `POST /api/v1/auth/refresh` - Refresh access token +- `POST /api/v1/auth/logout` - Logout user +- `PUT /api/v1/auth/password` - Change password + +### Users +- `GET /api/v1/users/me` - Get current user +- `GET /api/v1/users` - List users (admin) +- `GET /api/v1/users/:id` - Get user by ID (admin) +- `PUT /api/v1/users/:id` - Update user (admin) +- `DELETE /api/v1/users/:id` - Delete user (admin) + +### Health +- `GET /health` - Health check +- `GET /health/ready` - Readiness probe +- `GET /health/live` - Liveness probe + +## Development + +### Setup Environment Variables + +This service uses **Hybrid Environment Configuration**: + +**Step 1: Setup shared environment (from project root)** +```bash +# Copy shared environment template +cp deployments/local/env.local.example deployments/local/.env.local + +# Edit and add JWT secrets (must be same across all services) +# JWT_SECRET, JWT_REFRESH_SECRET, REDIS_HOST, etc. +``` + +**Step 2: Setup service-specific environment** +```bash +# Copy service environment template +cp env.local.example .env.local + +# Edit and add: +# - DATABASE_URL: Your Neon database connection string +# - PORT: 5001 +# - REDIS_HOST: localhost (for native dev) +``` + +**Environment structure:** +- `deployments/local/.env.local` → Shared configs (JWT, Redis, common) +- `services/auth-service/.env.local` → Service-specific (DATABASE_URL, PORT) + +### Run Development Server + +```bash +# From project root +pnpm --filter @goodgo/auth-service dev + +# Or from this directory +pnpm dev +``` + +The service will load env from both files automatically. + +### Database Setup + +```bash +# Generate Prisma client +pnpm prisma:generate + +# Run migrations +pnpm prisma:migrate + +# Seed database (optional) +pnpm prisma:seed + +# Open Prisma Studio +pnpm prisma studio +``` + +### Testing + +```bash +# Run tests +pnpm test + +# Watch mode +pnpm test:watch + +# Coverage +pnpm test:coverage +``` + +### Build + +```bash +# Build for production +pnpm build + +# Start production server +pnpm start +``` + +## Environment Variables + +### Shared Environment (`deployments/local/.env.local`) +- `JWT_SECRET` - JWT signing secret (must be same across services) +- `JWT_REFRESH_SECRET` - Refresh token secret +- `REDIS_HOST` - Redis hostname (redis for Docker) +- `NODE_ENV` - development/production +- `LOG_LEVEL` - debug/info/warn/error + +### Service-Specific Environment (`.env.local`) +- `DATABASE_URL` - Neon PostgreSQL connection string +- `PORT` - Service port (default: 5001) +- `SERVICE_NAME` - auth-service +- `REDIS_HOST` - localhost (override for native dev) + +See `env.local.example` for complete list. diff --git a/services/auth-service/env.local.example b/services/auth-service/env.local.example new file mode 100644 index 00000000..0ae2b535 --- /dev/null +++ b/services/auth-service/env.local.example @@ -0,0 +1,49 @@ +# Auth Service - Service-Specific Environment Variables +# Service-specific Environment Variables - Auth Service only +# Copy this file to .env.local and fill in your values +# +# Note: Shared configs (JWT_SECRET, REDIS_HOST for Docker) are in deployments/local/.env.local + +# ============================================================================= +# DATABASE - Service-specific Neon database +# ============================================================================= +# Get connection string from Neon Console: https://console.neon.tech +# Create a separate database for auth-service (e.g., goodgo_auth_dev) +DATABASE_URL=postgresql://user:password@ep-xxx.region.neon.tech/goodgo_auth_dev?sslmode=require&pgbouncer=true + +# ============================================================================= +# SERVICE CONFIGURATION +# ============================================================================= +PORT=5001 +SERVICE_NAME=auth-service +API_VERSION=v1 + +# ============================================================================= +# REDIS OVERRIDE - For native development (Redis in Docker) +# ============================================================================= +# When running service natively (not in Docker), Redis is in Docker container +# so we need to use localhost instead of 'redis' hostname +REDIS_HOST=localhost +# REDIS_PORT and REDIS_PASSWORD will be inherited from shared .env.local + +# ============================================================================= +# MONITORING (Optional - Override if needed) +# ============================================================================= +PROMETHEUS_PORT=9090 + +# ============================================================================= +# EXTERNAL SERVICES (Optional) +# ============================================================================= +EMAIL_SERVICE_URL=http://notification-service:5003 + +# ============================================================================= +# NOTES +# ============================================================================= +# 1. This file contains service-specific configs only +# 2. Shared configs are loaded from: deployments/local/.env.local +# - JWT_SECRET, JWT_REFRESH_SECRET (must be same across services) +# - REDIS_HOST=redis (for Docker), REDIS_PORT, REDIS_PASSWORD +# - NODE_ENV, LOG_LEVEL, CORS_ORIGIN +# 3. DATABASE_URL should point to auth-service's own database +# 4. REDIS_HOST=localhost overrides the shared 'redis' for native dev +# 5. When running in Docker, remove REDIS_HOST override (use shared config) diff --git a/services/auth-service/package.json b/services/auth-service/package.json new file mode 100644 index 00000000..165936f6 --- /dev/null +++ b/services/auth-service/package.json @@ -0,0 +1,54 @@ +{ + "name": "@goodgo/auth-service", + "version": "1.0.0", + "description": "Authentication and Authorization Service", + "main": "./dist/main.js", + "scripts": { + "dev": "dotenv -e ../../deployments/local/.env.local -e .env.local -- tsx watch src/main.ts", + "build": "tsc", + "start": "node dist/main.js", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint src --ext .ts", + "typecheck": "tsc --noEmit", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:deploy": "prisma migrate deploy", + "prisma:seed": "dotenv -e ../../deployments/local/.env.local -e .env.local -- tsx prisma/seed.ts", + "clean": "rm -rf dist" + }, + "dependencies": { + "@goodgo/auth-sdk": "workspace:*", + "@goodgo/logger": "workspace:*", + "@goodgo/tracing": "workspace:*", + "@goodgo/types": "workspace:*", + "@prisma/client": "^5.9.1", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "helmet": "^7.1.0", + "ioredis": "^5.3.2", + "jsonwebtoken": "^9.0.2", + "zod": "^3.22.4" + }, + "devDependencies": { + "@goodgo/eslint-config": "workspace:*", + "@goodgo/tsconfig": "workspace:*", + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.11", + "@types/jsonwebtoken": "^9.0.5", + "@types/node": "^20.11.0", + "@types/supertest": "^2.0.16", + "dotenv-cli": "^11.0.0", + "jest": "^29.7.0", + "prisma": "^5.9.1", + "supertest": "^6.3.3", + "ts-jest": "^29.1.2", + "tsx": "^4.7.1", + "typescript": "^5.3.3" + } +} diff --git a/services/auth-service/prisma/schema.prisma b/services/auth-service/prisma/schema.prisma new file mode 100644 index 00000000..6752435b --- /dev/null +++ b/services/auth-service/prisma/schema.prisma @@ -0,0 +1,58 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(uuid()) + email String @unique + passwordHash String + role Role @default(USER) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + sessions Session[] + refreshTokens RefreshToken[] + + @@index([email]) + @@map("users") +} + +model Session { + id String @id @default(uuid()) + userId String + token String @unique + expiresAt DateTime + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([token]) + @@map("sessions") +} + +model RefreshToken { + id String @id @default(uuid()) + userId String + token String @unique + expiresAt DateTime + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([token]) + @@map("refresh_tokens") +} + +enum Role { + USER + ADMIN + SUPER_ADMIN +} diff --git a/services/auth-service/prisma/seed.d.ts b/services/auth-service/prisma/seed.d.ts new file mode 100644 index 00000000..0986b1c3 --- /dev/null +++ b/services/auth-service/prisma/seed.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=seed.d.ts.map \ No newline at end of file diff --git a/services/auth-service/prisma/seed.d.ts.map b/services/auth-service/prisma/seed.d.ts.map new file mode 100644 index 00000000..c1a0e0b3 --- /dev/null +++ b/services/auth-service/prisma/seed.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["seed.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/services/auth-service/prisma/seed.js b/services/auth-service/prisma/seed.js new file mode 100644 index 00000000..5fdf8162 --- /dev/null +++ b/services/auth-service/prisma/seed.js @@ -0,0 +1,31 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const client_1 = require("@prisma/client"); +const bcryptjs_1 = __importDefault(require("bcryptjs")); +const prisma = new client_1.PrismaClient(); +async function main() { + const hashedPassword = await bcryptjs_1.default.hash('Admin@123456', 10); + const admin = await prisma.user.upsert({ + where: { email: 'admin@thewann.com' }, + update: {}, + create: { + email: 'admin@thewann.com', + passwordHash: hashedPassword, + role: client_1.Role.ADMIN, + isActive: true, + }, + }); + console.log('Seeded admin user:', admin); +} +main() + .catch((e) => { + console.error(e); + process.exit(1); +}) + .finally(async () => { + await prisma.$disconnect(); +}); +//# sourceMappingURL=seed.js.map \ No newline at end of file diff --git a/services/auth-service/prisma/seed.js.map b/services/auth-service/prisma/seed.js.map new file mode 100644 index 00000000..7ded5c04 --- /dev/null +++ b/services/auth-service/prisma/seed.js.map @@ -0,0 +1 @@ +{"version":3,"file":"seed.js","sourceRoot":"","sources":["seed.ts"],"names":[],"mappings":";;;;;AAAA,2CAAoD;AACpD,wDAA8B;AAE9B,MAAM,MAAM,GAAG,IAAI,qBAAY,EAAE,CAAC;AAElC,KAAK,UAAU,IAAI;IACjB,MAAM,cAAc,GAAG,MAAM,kBAAM,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IAE7D,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;QACrC,KAAK,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE;QACrC,MAAM,EAAE,EAAE;QACV,MAAM,EAAE;YACN,KAAK,EAAE,mBAAmB;YAC1B,YAAY,EAAE,cAAc;YAC5B,IAAI,EAAE,aAAI,CAAC,KAAK;YAChB,QAAQ,EAAE,IAAI;SACf;KACF,CAAC,CAAC;IAEH,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,KAAK,CAAC,CAAC;AAC3C,CAAC;AAED,IAAI,EAAE;KACH,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;IACX,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC;KACD,OAAO,CAAC,KAAK,IAAI,EAAE;IAClB,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;AAC7B,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/services/auth-service/prisma/seed.ts b/services/auth-service/prisma/seed.ts new file mode 100644 index 00000000..6d99ab2c --- /dev/null +++ b/services/auth-service/prisma/seed.ts @@ -0,0 +1,30 @@ +import { PrismaClient, Role } from '@prisma/client'; +import bcrypt from 'bcryptjs'; + +const prisma = new PrismaClient(); + +async function main() { + const hashedPassword = await bcrypt.hash('Admin@123456', 10); + + const admin = await prisma.user.upsert({ + where: { email: 'admin@thewann.com' }, + update: {}, + create: { + email: 'admin@thewann.com', + passwordHash: hashedPassword, + role: Role.ADMIN, + isActive: true, + }, + }); + + console.log('Seeded admin user:', admin); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/services/auth-service/src/config/app.config.ts b/services/auth-service/src/config/app.config.ts new file mode 100644 index 00000000..a88bbc17 --- /dev/null +++ b/services/auth-service/src/config/app.config.ts @@ -0,0 +1,6 @@ +export const appConfig = { + port: parseInt(process.env.PORT || '5001', 10), + nodeEnv: process.env.NODE_ENV || 'development', + apiVersion: process.env.API_VERSION || 'v1', + corsOrigin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:3000'], +}; diff --git a/services/auth-service/src/config/database.config.ts b/services/auth-service/src/config/database.config.ts new file mode 100644 index 00000000..6c5d23ee --- /dev/null +++ b/services/auth-service/src/config/database.config.ts @@ -0,0 +1,21 @@ +import { PrismaClient } from '@prisma/client'; +import { logger } from '@goodgo/logger'; + +export const prisma = new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], +}); + +export const connectDatabase = async (): Promise => { + try { + await prisma.$connect(); + logger.info('Database connected successfully'); + } catch (error) { + logger.error('Database connection failed', { error }); + process.exit(1); + } +}; + +export const disconnectDatabase = async (): Promise => { + await prisma.$disconnect(); + logger.info('Database disconnected'); +}; diff --git a/services/auth-service/src/config/jwt.config.ts b/services/auth-service/src/config/jwt.config.ts new file mode 100644 index 00000000..fb057306 --- /dev/null +++ b/services/auth-service/src/config/jwt.config.ts @@ -0,0 +1,10 @@ +export const jwtConfig = { + secret: process.env.JWT_SECRET || 'default-secret-change-in-production', + expiresIn: process.env.JWT_EXPIRES_IN || '15m', + refreshSecret: process.env.JWT_REFRESH_SECRET || 'default-refresh-secret-change-in-production', + refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d', +}; + +if (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'default-secret-change-in-production') { + console.warn('⚠️ WARNING: Using default JWT_SECRET. Change it in production!'); +} diff --git a/services/auth-service/src/main.ts b/services/auth-service/src/main.ts new file mode 100644 index 00000000..2b39aeef --- /dev/null +++ b/services/auth-service/src/main.ts @@ -0,0 +1,81 @@ +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import rateLimit from 'express-rate-limit'; +import { connectDatabase } from './config/database.config'; +import { appConfig } from './config/app.config'; +import { createRouter } from './routes'; +import { requestLogger } from './middlewares/logger.middleware'; +import { errorHandler, notFoundHandler } from './middlewares/error.middleware'; +import { logger } from '@goodgo/logger'; +import { initTracing } from '@goodgo/tracing'; + +// Initialize tracing +if (process.env.TRACING_ENABLED === 'true') { + initTracing({ + serviceName: process.env.SERVICE_NAME || 'auth-service', + jaegerEndpoint: process.env.JAEGER_ENDPOINT, + enabled: true, + }); +} + +const app = express(); + +// Security middleware +app.use(helmet()); +app.use( + cors({ + origin: appConfig.corsOrigin, + credentials: true, + }) +); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs +}); +app.use('/api', limiter); + +// Body parsing +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// Request logging +app.use(requestLogger); + +// Routes +app.use(createRouter()); + +// Error handling +app.use(notFoundHandler); +app.use(errorHandler); + +const startServer = async () => { + try { + await connectDatabase(); + + app.listen(appConfig.port, () => { + logger.info(`Auth Service started on port ${appConfig.port}`, { + port: appConfig.port, + nodeEnv: appConfig.nodeEnv, + }); + }); + } catch (error) { + logger.error('Failed to start server', { error }); + process.exit(1); + } +}; + +startServer(); + +// Graceful shutdown +process.on('SIGTERM', async () => { + logger.info('SIGTERM received, shutting down gracefully'); + process.exit(0); +}); + +process.on('SIGINT', async () => { + logger.info('SIGINT received, shutting down gracefully'); + process.exit(0); +}); diff --git a/services/auth-service/src/middlewares/auth.middleware.ts b/services/auth-service/src/middlewares/auth.middleware.ts new file mode 100644 index 00000000..40b447fa --- /dev/null +++ b/services/auth-service/src/middlewares/auth.middleware.ts @@ -0,0 +1,86 @@ +import { Request, Response, NextFunction } from 'express'; +import { verifyToken, extractTokenFromHeader } from '@goodgo/auth-sdk'; +import { jwtConfig } from '../config/jwt.config'; +import { logger } from '@goodgo/logger'; + +export interface AuthRequest extends Request { + user?: { + userId: string; + email: string; + role: string; + }; +} + +export const authenticate = async ( + req: AuthRequest, + res: Response, + next: NextFunction +): Promise => { + try { + const token = extractTokenFromHeader(req.headers.authorization); + + if (!token) { + res.status(401).json({ + success: false, + error: { + code: 'AUTH_001', + message: 'Authentication token required', + }, + timestamp: new Date().toISOString(), + }); + return; + } + + const payload = verifyToken(token, { + secret: jwtConfig.secret, + }); + + req.user = { + userId: payload.userId, + email: payload.email, + role: payload.role, + }; + + next(); + } catch (error) { + logger.error('Authentication failed', { error }); + res.status(401).json({ + success: false, + error: { + code: 'AUTH_002', + message: 'Invalid or expired token', + }, + timestamp: new Date().toISOString(), + }); + } +}; + +export const authorize = (...roles: string[]) => { + return (req: AuthRequest, res: Response, next: NextFunction): void => { + if (!req.user) { + res.status(401).json({ + success: false, + error: { + code: 'AUTH_003', + message: 'Authentication required', + }, + timestamp: new Date().toISOString(), + }); + return; + } + + if (!roles.includes(req.user.role)) { + res.status(403).json({ + success: false, + error: { + code: 'AUTH_004', + message: 'Insufficient permissions', + }, + timestamp: new Date().toISOString(), + }); + return; + } + + next(); + }; +}; diff --git a/services/auth-service/src/middlewares/error.middleware.ts b/services/auth-service/src/middlewares/error.middleware.ts new file mode 100644 index 00000000..ef0a6549 --- /dev/null +++ b/services/auth-service/src/middlewares/error.middleware.ts @@ -0,0 +1,42 @@ +import { Request, Response, NextFunction } from 'express'; +import { logger } from '@goodgo/logger'; +import { ApiResponse } from '@goodgo/types'; + +export const errorHandler = ( + err: Error, + req: Request, + res: Response, + _next: NextFunction +): void => { + logger.error('Error occurred', { + error: err.message, + stack: err.stack, + path: req.path, + method: req.method, + }); + + const response: ApiResponse = { + success: false, + error: { + code: 'INTERNAL_ERROR', + message: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message, + details: process.env.NODE_ENV === 'development' ? { stack: err.stack } : undefined, + }, + timestamp: new Date().toISOString(), + }; + + res.status(500).json(response); +}; + +export const notFoundHandler = (req: Request, res: Response): void => { + const response: ApiResponse = { + success: false, + error: { + code: 'NOT_FOUND', + message: `Route ${req.path} not found`, + }, + timestamp: new Date().toISOString(), + }; + + res.status(404).json(response); +}; diff --git a/services/auth-service/src/middlewares/logger.middleware.ts b/services/auth-service/src/middlewares/logger.middleware.ts new file mode 100644 index 00000000..d7dd0c20 --- /dev/null +++ b/services/auth-service/src/middlewares/logger.middleware.ts @@ -0,0 +1,19 @@ +import { Request, Response, NextFunction } from 'express'; +import { logger } from '@goodgo/logger'; + +export const requestLogger = (req: Request, res: Response, next: NextFunction): void => { + const start = Date.now(); + + res.on('finish', () => { + const duration = Date.now() - start; + logger.info('HTTP Request', { + method: req.method, + path: req.path, + statusCode: res.statusCode, + duration: `${duration}ms`, + ip: req.ip, + }); + }); + + next(); +}; diff --git a/services/auth-service/src/modules/auth/auth.controller.ts b/services/auth-service/src/modules/auth/auth.controller.ts new file mode 100644 index 00000000..93cc4887 --- /dev/null +++ b/services/auth-service/src/modules/auth/auth.controller.ts @@ -0,0 +1,142 @@ +import { Request, Response } from 'express'; +import { AuthService } from './auth.service'; +import { + loginDtoSchema, + registerDtoSchema, + refreshTokenDtoSchema, + changePasswordDtoSchema, +} from './auth.dto'; +import { AuthRequest } from '../../middlewares/auth.middleware'; +import { ApiResponse } from '@goodgo/types'; +import { AuthResponse } from './auth.dto'; + +export class AuthController { + private authService: AuthService; + + constructor() { + this.authService = new AuthService(); + } + + register = async (req: Request, res: Response): Promise => { + try { + const data = registerDtoSchema.parse(req.body); + const result = await this.authService.register(data); + + const response: ApiResponse = { + success: true, + data: result, + message: 'User registered successfully', + timestamp: new Date().toISOString(), + }; + + res.status(201).json(response); + } catch (error: any) { + res.status(400).json({ + success: false, + error: { + code: 'AUTH_005', + message: error.message || 'Registration failed', + }, + timestamp: new Date().toISOString(), + }); + } + }; + + login = async (req: Request, res: Response): Promise => { + try { + const data = loginDtoSchema.parse(req.body); + const result = await this.authService.login(data); + + const response: ApiResponse = { + success: true, + data: result, + message: 'Login successful', + timestamp: new Date().toISOString(), + }; + + res.json(response); + } catch (error: any) { + res.status(401).json({ + success: false, + error: { + code: 'AUTH_006', + message: error.message || 'Login failed', + }, + timestamp: new Date().toISOString(), + }); + } + }; + + refresh = async (req: Request, res: Response): Promise => { + try { + const data = refreshTokenDtoSchema.parse(req.body); + const result = await this.authService.refreshToken(data); + + const response: ApiResponse<{ accessToken: string }> = { + success: true, + data: result, + message: 'Token refreshed successfully', + timestamp: new Date().toISOString(), + }; + + res.json(response); + } catch (error: any) { + res.status(401).json({ + success: false, + error: { + code: 'AUTH_007', + message: error.message || 'Token refresh failed', + }, + timestamp: new Date().toISOString(), + }); + } + }; + + logout = async (req: AuthRequest, res: Response): Promise => { + try { + const refreshToken = req.body.refreshToken; + await this.authService.logout(req.user!.userId, refreshToken); + + const response: ApiResponse = { + success: true, + message: 'Logout successful', + timestamp: new Date().toISOString(), + }; + + res.json(response); + } catch (error: any) { + res.status(500).json({ + success: false, + error: { + code: 'AUTH_008', + message: error.message || 'Logout failed', + }, + timestamp: new Date().toISOString(), + }); + } + }; + + changePassword = async (req: AuthRequest, res: Response): Promise => { + try { + const data = changePasswordDtoSchema.parse(req.body); + await this.authService.changePassword(req.user!.userId, data); + + const response: ApiResponse = { + success: true, + message: 'Password changed successfully', + timestamp: new Date().toISOString(), + }; + + res.json(response); + } catch (error: any) { + res.status(400).json({ + success: false, + error: { + code: 'AUTH_009', + message: error.message || 'Password change failed', + }, + timestamp: new Date().toISOString(), + }); + } + }; +} diff --git a/services/auth-service/src/modules/auth/auth.dto.ts b/services/auth-service/src/modules/auth/auth.dto.ts new file mode 100644 index 00000000..e703cf7b --- /dev/null +++ b/services/auth-service/src/modules/auth/auth.dto.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; + +export const loginDtoSchema = z.object({ + email: z.string().email('Invalid email format'), + password: z.string().min(6, 'Password must be at least 6 characters'), +}); + +export const registerDtoSchema = z.object({ + email: z.string().email('Invalid email format'), + password: z.string().min(6, 'Password must be at least 6 characters'), + confirmPassword: z.string(), +}).refine((data) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ['confirmPassword'], +}); + +export const refreshTokenDtoSchema = z.object({ + refreshToken: z.string(), +}); + +export const changePasswordDtoSchema = z.object({ + currentPassword: z.string(), + newPassword: z.string().min(6, 'Password must be at least 6 characters'), +}); + +export const forgotPasswordDtoSchema = z.object({ + email: z.string().email('Invalid email format'), +}); + +export const resetPasswordDtoSchema = z.object({ + token: z.string(), + newPassword: z.string().min(6, 'Password must be at least 6 characters'), +}); + +export type LoginDto = z.infer; +export type RegisterDto = z.infer; +export type RefreshTokenDto = z.infer; +export type ChangePasswordDto = z.infer; +export type ForgotPasswordDto = z.infer; +export type ResetPasswordDto = z.infer; + +// Re-export AuthResponse from @goodgo/types +export { AuthResponse } from '@goodgo/types'; diff --git a/services/auth-service/src/modules/auth/auth.module.ts b/services/auth-service/src/modules/auth/auth.module.ts new file mode 100644 index 00000000..ec0038b4 --- /dev/null +++ b/services/auth-service/src/modules/auth/auth.module.ts @@ -0,0 +1,16 @@ +import { Router } from 'express'; +import { AuthController } from './auth.controller'; +import { authenticate } from '../../middlewares/auth.middleware'; + +export const createAuthRouter = (): Router => { + const router = Router(); + const authController = new AuthController(); + + router.post('/register', authController.register); + router.post('/login', authController.login); + router.post('/refresh', authController.refresh); + router.post('/logout', authenticate, authController.logout); + router.put('/password', authenticate, authController.changePassword); + + return router; +}; diff --git a/services/auth-service/src/modules/auth/auth.service.ts b/services/auth-service/src/modules/auth/auth.service.ts new file mode 100644 index 00000000..e6b13d19 --- /dev/null +++ b/services/auth-service/src/modules/auth/auth.service.ts @@ -0,0 +1,206 @@ +import bcrypt from 'bcryptjs'; +import { createToken } from '@goodgo/auth-sdk'; +import { jwtConfig } from '../../config/jwt.config'; +import { logger } from '@goodgo/logger'; +import { AuthResponse, Role as TypeRole } from '@goodgo/types'; +import { + LoginDto, + RegisterDto, + RefreshTokenDto, + ChangePasswordDto, +} from './auth.dto'; +import { prisma } from '../../config/database.config'; + +export class AuthService { + async register(data: RegisterDto): Promise { + const existingUser = await prisma.user.findUnique({ + where: { email: data.email }, + }); + + if (existingUser) { + throw new Error('User already exists'); + } + + const passwordHash = await bcrypt.hash(data.password, 10); + + const user = await prisma.user.create({ + data: { + email: data.email, + passwordHash, + role: 'USER', + }, + }); + + const accessToken = createToken( + { + userId: user.id, + email: user.email, + role: user.role, + }, + jwtConfig.secret, + jwtConfig.expiresIn + ); + + const refreshToken = createToken( + { + userId: user.id, + email: user.email, + role: user.role, + }, + jwtConfig.refreshSecret, + jwtConfig.refreshExpiresIn + ); + + await prisma.refreshToken.create({ + data: { + userId: user.id, + token: refreshToken, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days + }, + }); + + logger.info('User registered', { userId: user.id, email: user.email }); + + return { + accessToken, + refreshToken, + user: { + id: user.id, + email: user.email, + role: user.role as TypeRole, + isActive: user.isActive, + createdAt: user.createdAt.toISOString(), + updatedAt: user.updatedAt.toISOString(), + }, + }; + } + + async login(data: LoginDto): Promise { + const user = await prisma.user.findUnique({ + where: { email: data.email }, + }); + + if (!user || !user.isActive) { + throw new Error('Invalid credentials'); + } + + const isValidPassword = await bcrypt.compare(data.password, user.passwordHash); + + if (!isValidPassword) { + throw new Error('Invalid credentials'); + } + + const accessToken = createToken( + { + userId: user.id, + email: user.email, + role: user.role, + }, + jwtConfig.secret, + jwtConfig.expiresIn + ); + + const refreshToken = createToken( + { + userId: user.id, + email: user.email, + role: user.role, + }, + jwtConfig.refreshSecret, + jwtConfig.refreshExpiresIn + ); + + await prisma.refreshToken.create({ + data: { + userId: user.id, + token: refreshToken, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }, + }); + + logger.info('User logged in', { userId: user.id, email: user.email }); + + return { + accessToken, + refreshToken, + user: { + id: user.id, + email: user.email, + role: user.role as TypeRole, + isActive: user.isActive, + createdAt: user.createdAt.toISOString(), + updatedAt: user.updatedAt.toISOString(), + }, + }; + } + + async refreshToken(data: RefreshTokenDto): Promise<{ accessToken: string }> { + const refreshTokenRecord = await prisma.refreshToken.findUnique({ + where: { token: data.refreshToken }, + include: { user: true }, + }); + + if (!refreshTokenRecord || refreshTokenRecord.expiresAt < new Date()) { + throw new Error('Invalid or expired refresh token'); + } + + const accessToken = createToken( + { + userId: refreshTokenRecord.user.id, + email: refreshTokenRecord.user.email, + role: refreshTokenRecord.user.role, + }, + jwtConfig.secret, + jwtConfig.expiresIn + ); + + return { accessToken }; + } + + async logout(userId: string, refreshToken?: string): Promise { + if (refreshToken) { + await prisma.refreshToken.deleteMany({ + where: { + userId, + token: refreshToken, + }, + }); + } else { + await prisma.refreshToken.deleteMany({ + where: { userId }, + }); + } + + logger.info('User logged out', { userId }); + } + + async changePassword(userId: string, data: ChangePasswordDto): Promise { + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + throw new Error('User not found'); + } + + const isValidPassword = await bcrypt.compare(data.currentPassword, user.passwordHash); + + if (!isValidPassword) { + throw new Error('Current password is incorrect'); + } + + const newPasswordHash = await bcrypt.hash(data.newPassword, 10); + + await prisma.user.update({ + where: { id: userId }, + data: { passwordHash: newPasswordHash }, + }); + + // Invalidate all refresh tokens + await prisma.refreshToken.deleteMany({ + where: { userId }, + }); + + logger.info('Password changed', { userId }); + } +} diff --git a/services/auth-service/src/modules/health/health.controller.ts b/services/auth-service/src/modules/health/health.controller.ts new file mode 100644 index 00000000..1b08ffa7 --- /dev/null +++ b/services/auth-service/src/modules/health/health.controller.ts @@ -0,0 +1,46 @@ +import { Request, Response } from 'express'; +import { prisma } from '../../config/database.config'; +import { ApiResponse } from '@goodgo/types'; + +export class HealthController { + health = async (_req: Request, res: Response): Promise => { + const response: ApiResponse<{ status: string; timestamp: string }> = { + success: true, + data: { + status: 'ok', + timestamp: new Date().toISOString(), + }, + timestamp: new Date().toISOString(), + }; + + res.json(response); + }; + + ready = async (_req: Request, res: Response): Promise => { + try { + await prisma.$queryRaw`SELECT 1`; + res.json({ + success: true, + data: { status: 'ready' }, + timestamp: new Date().toISOString(), + }); + } catch (error) { + res.status(503).json({ + success: false, + error: { + code: 'HEALTH_001', + message: 'Service not ready', + }, + timestamp: new Date().toISOString(), + }); + } + }; + + live = async (_req: Request, res: Response): Promise => { + res.json({ + success: true, + data: { status: 'live' }, + timestamp: new Date().toISOString(), + }); + }; +} diff --git a/services/auth-service/src/modules/user/user.controller.ts b/services/auth-service/src/modules/user/user.controller.ts new file mode 100644 index 00000000..db10fca6 --- /dev/null +++ b/services/auth-service/src/modules/user/user.controller.ts @@ -0,0 +1,134 @@ +import { Request, Response } from 'express'; +import { UserService } from './user.service'; +import { updateUserDtoSchema } from './user.dto'; +import { AuthRequest } from '../../middlewares/auth.middleware'; +import { ApiResponse, PaginatedResponse, UserResponse } from '@goodgo/types'; + +export class UserController { + private userService: UserService; + + constructor() { + this.userService = new UserService(); + } + + getMe = async (req: AuthRequest, res: Response): Promise => { + try { + const user = await this.userService.getCurrentUser(req.user!.userId); + + const response: ApiResponse = { + success: true, + data: user, + timestamp: new Date().toISOString(), + }; + + res.json(response); + } catch (error: any) { + res.status(404).json({ + success: false, + error: { + code: 'USER_001', + message: error.message || 'User not found', + }, + timestamp: new Date().toISOString(), + }); + } + }; + + getUsers = async (req: Request, res: Response): Promise => { + try { + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 20; + + const result = await this.userService.getUsers(page, limit); + + const response: PaginatedResponse = { + success: true, + data: result.data, + pagination: result.pagination, + timestamp: new Date().toISOString(), + }; + + res.json(response); + } catch (error: any) { + res.status(500).json({ + success: false, + error: { + code: 'USER_002', + message: error.message || 'Failed to fetch users', + }, + timestamp: new Date().toISOString(), + }); + } + }; + + getUserById = async (req: Request, res: Response): Promise => { + try { + const user = await this.userService.getUserById(req.params.id); + + const response: ApiResponse = { + success: true, + data: user, + timestamp: new Date().toISOString(), + }; + + res.json(response); + } catch (error: any) { + res.status(404).json({ + success: false, + error: { + code: 'USER_003', + message: error.message || 'User not found', + }, + timestamp: new Date().toISOString(), + }); + } + }; + + updateUser = async (req: Request, res: Response): Promise => { + try { + const data = updateUserDtoSchema.parse(req.body); + const user = await this.userService.updateUser(req.params.id, data); + + const response: ApiResponse = { + success: true, + data: user, + message: 'User updated successfully', + timestamp: new Date().toISOString(), + }; + + res.json(response); + } catch (error: any) { + res.status(400).json({ + success: false, + error: { + code: 'USER_004', + message: error.message || 'Failed to update user', + }, + timestamp: new Date().toISOString(), + }); + } + }; + + deleteUser = async (req: Request, res: Response): Promise => { + try { + await this.userService.deleteUser(req.params.id); + + const response: ApiResponse = { + success: true, + message: 'User deleted successfully', + timestamp: new Date().toISOString(), + }; + + res.json(response); + } catch (error: any) { + res.status(500).json({ + success: false, + error: { + code: 'USER_005', + message: error.message || 'Failed to delete user', + }, + timestamp: new Date().toISOString(), + }); + } + }; +} diff --git a/services/auth-service/src/modules/user/user.dto.ts b/services/auth-service/src/modules/user/user.dto.ts new file mode 100644 index 00000000..689f82a5 --- /dev/null +++ b/services/auth-service/src/modules/user/user.dto.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const updateUserDtoSchema = z.object({ + email: z.string().email().optional(), + role: z.enum(['USER', 'ADMIN', 'SUPER_ADMIN']).optional(), + isActive: z.boolean().optional(), +}); + +export type UpdateUserDto = z.infer; diff --git a/services/auth-service/src/modules/user/user.module.ts b/services/auth-service/src/modules/user/user.module.ts new file mode 100644 index 00000000..d57bc0fa --- /dev/null +++ b/services/auth-service/src/modules/user/user.module.ts @@ -0,0 +1,16 @@ +import { Router } from 'express'; +import { UserController } from './user.controller'; +import { authenticate, authorize } from '../../middlewares/auth.middleware'; + +export const createUserRouter = (): Router => { + const router = Router(); + const userController = new UserController(); + + router.get('/me', authenticate, userController.getMe); + router.get('/', authenticate, authorize('ADMIN', 'SUPER_ADMIN'), userController.getUsers); + router.get('/:id', authenticate, authorize('ADMIN', 'SUPER_ADMIN'), userController.getUserById); + router.put('/:id', authenticate, authorize('ADMIN', 'SUPER_ADMIN'), userController.updateUser); + router.delete('/:id', authenticate, authorize('ADMIN', 'SUPER_ADMIN'), userController.deleteUser); + + return router; +}; diff --git a/services/auth-service/src/modules/user/user.service.ts b/services/auth-service/src/modules/user/user.service.ts new file mode 100644 index 00000000..7a0d91a4 --- /dev/null +++ b/services/auth-service/src/modules/user/user.service.ts @@ -0,0 +1,104 @@ +import { prisma } from '../../config/database.config'; +import { logger } from '@goodgo/logger'; +import { UpdateUserDto } from './user.dto'; +import { UserResponse, Role as TypeRole } from '@goodgo/types'; + +export class UserService { + async getCurrentUser(userId: string): Promise { + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + throw new Error('User not found'); + } + + return { + id: user.id, + email: user.email, + role: user.role as TypeRole, + isActive: user.isActive, + createdAt: user.createdAt.toISOString(), + updatedAt: user.updatedAt.toISOString(), + }; + } + + async getUserById(id: string): Promise { + const user = await prisma.user.findUnique({ + where: { id }, + }); + + if (!user) { + throw new Error('User not found'); + } + + return { + id: user.id, + email: user.email, + role: user.role as TypeRole, + isActive: user.isActive, + createdAt: user.createdAt.toISOString(), + updatedAt: user.updatedAt.toISOString(), + }; + } + + async getUsers(page: number = 1, limit: number = 20) { + const skip = (page - 1) * limit; + + const [users, total] = await Promise.all([ + prisma.user.findMany({ + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + }), + prisma.user.count(), + ]); + + return { + data: users.map((user) => ({ + id: user.id, + email: user.email, + role: user.role as TypeRole, + isActive: user.isActive, + createdAt: user.createdAt.toISOString(), + updatedAt: user.updatedAt.toISOString(), + })), + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + }, + }; + } + + async updateUser(id: string, data: UpdateUserDto): Promise { + const user = await prisma.user.update({ + where: { id }, + data: { + email: data.email, + role: data.role as any, + isActive: data.isActive, + }, + }); + + logger.info('User updated', { userId: id }); + + return { + id: user.id, + email: user.email, + role: user.role as TypeRole, + isActive: user.isActive, + createdAt: user.createdAt.toISOString(), + updatedAt: user.updatedAt.toISOString(), + }; + } + + async deleteUser(id: string): Promise { + await prisma.user.delete({ + where: { id }, + }); + + logger.info('User deleted', { userId: id }); + } +} diff --git a/services/auth-service/src/routes/index.ts b/services/auth-service/src/routes/index.ts new file mode 100644 index 00000000..519cfaa0 --- /dev/null +++ b/services/auth-service/src/routes/index.ts @@ -0,0 +1,22 @@ +import { Router } from 'express'; +import { createAuthRouter } from '../modules/auth/auth.module'; +import { createUserRouter } from '../modules/user/user.module'; +import { HealthController } from '../modules/health/health.controller'; + +export const createRouter = (): Router => { + const router = Router(); + const healthController = new HealthController(); + + const apiVersion = process.env.API_VERSION || 'v1'; + + // Health checks + router.get('/health', healthController.health); + router.get('/health/ready', healthController.ready); + router.get('/health/live', healthController.live); + + // API routes + router.use(`/api/${apiVersion}/auth`, createAuthRouter()); + router.use(`/api/${apiVersion}/users`, createUserRouter()); + + return router; +}; diff --git a/services/auth-service/tsconfig.json b/services/auth-service/tsconfig.json new file mode 100644 index 00000000..50a94dde --- /dev/null +++ b/services/auth-service/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@goodgo/tsconfig/node.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests", "prisma"] +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 00000000..4c42f8ba --- /dev/null +++ b/turbo.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", ".next/**", "build/**"] + }, + "test": { + "dependsOn": ["build"], + "outputs": [] + }, + "lint": { + "outputs": [] + }, + "typecheck": { + "dependsOn": ["^build"], + "outputs": [] + }, + "dev": { + "cache": false, + "persistent": true + }, + "clean": { + "cache": false + } + } +}