From 04472fcd2b06c31713a077b0ef0cb3becda26b23 Mon Sep 17 00:00:00 2001 From: Tiago Date: Fri, 14 Aug 2026 17:35:24 -0300 Subject: [PATCH] first commit --- .env.example | 70 + .gitignore | 24 + .prettierrc | 5 + Dockerfile | 49 + Etapas/00-README-ordem-das-etapas.md | 57 + Etapas/01-setup-projeto.md | 115 + Etapas/02-autenticacao-rls.md | 115 + Etapas/03-painel-master-tenants.md | 98 + Etapas/04-integracoes-evolution-email.md | 85 + Etapas/05-painel-tenant-customers-produtos.md | 69 + Etapas/06-vendas-dashboards.md | 85 + Etapas/07-motor-de-alertas.md | 89 + Etapas/08-personalizacao-whitelabel.md | 87 + Etapas/09-controle-de-progresso.md | 601 + Etapas/seconectai-vendas-arquitetura.md | 118 + Etapas/seconectai-vendas-schema.sql | 451 + README.md | 172 + docker-compose.override.yml | 12 + docker-compose.yml | 55 + eslint.config.mjs | 34 + nest-cli.json | 8 + package-lock.json | 10319 ++++++++++++++++ package.json | 87 + .../20260812160945_init/migration.sql | 451 + .../migration.sql | 119 + .../migration.sql | 53 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 438 + src/app.module.ts | 37 + src/common/audit-log.util.ts | 19 + src/common/encryption.module.ts | 9 + src/common/encryption.service.spec.ts | 34 + src/common/encryption.service.ts | 52 + src/common/mask.util.ts | 10 + src/config/env.validation.ts | 76 + src/config/prisma-master.service.ts | 27 + src/config/prisma-tenant.service.ts | 32 + src/config/prisma.module.ts | 12 + src/config/prisma.service.ts | 13 + src/config/tenant-context.service.ts | 49 + src/main.ts | 22 + .../alert-dispatch/alert-dispatch.module.ts | 11 + .../alert-dispatch/alert-dispatch.service.ts | 102 + src/modules/auth/auth.controller.ts | 119 + src/modules/auth/auth.module.ts | 38 + src/modules/auth/auth.service.ts | 321 + .../auth/decorators/current-user.decorator.ts | 7 + .../auth/decorators/roles.decorator.ts | 5 + src/modules/auth/dto/forgot-password.dto.ts | 10 + src/modules/auth/dto/login.dto.ts | 10 + src/modules/auth/dto/logout.dto.ts | 6 + src/modules/auth/dto/refresh.dto.ts | 6 + src/modules/auth/dto/reset-password.dto.ts | 10 + src/modules/auth/guards/jwt-auth.guard.ts | 5 + src/modules/auth/guards/master.guard.ts | 16 + src/modules/auth/guards/roles.guard.ts | 31 + src/modules/auth/guards/tenant.guard.ts | 39 + .../interceptors/tenant-rls.interceptor.ts | 32 + src/modules/auth/password.util.ts | 47 + src/modules/auth/strategies/jwt.strategy.ts | 20 + src/modules/auth/time.util.ts | 10 + src/modules/auth/token.util.ts | 9 + .../auth/types/authenticated-request.type.ts | 8 + src/modules/auth/types/jwt-payload.type.ts | 10 + .../dto/create-master-email-config.dto.ts | 24 + .../dto/testar-envio-email.dto.ts | 6 + .../dto/update-master-email-config.dto.ts | 42 + .../dto/update-tenant-email-config.dto.ts | 47 + .../email-config/email-config.module.ts | 13 + src/modules/email-config/email.module.ts | 8 + src/modules/email-config/email.service.ts | 100 + .../master-email-config.controller.ts | 67 + .../master-email-config.service.ts | 120 + .../tenant-email-config.controller.ts | 37 + .../tenant-email-config.service.ts | 90 + .../dto/create-evolution-config.dto.ts | 20 + .../dto/update-evolution-config.dto.ts | 27 + src/modules/evolution/evolution-api.client.ts | 167 + src/modules/evolution/evolution.controller.ts | 76 + src/modules/evolution/evolution.module.ts | 11 + src/modules/evolution/evolution.service.ts | 151 + src/modules/health/health.controller.spec.ts | 35 + src/modules/health/health.controller.ts | 12 + src/modules/health/health.module.ts | 9 + src/modules/health/health.service.ts | 32 + src/modules/planos/dto/create-plano.dto.ts | 30 + .../planos/dto/list-planos.query.dto.ts | 9 + src/modules/planos/dto/update-plano.dto.ts | 45 + src/modules/planos/planos.controller.ts | 62 + src/modules/planos/planos.module.ts | 10 + src/modules/planos/planos.service.ts | 122 + src/modules/tenants/dashboard.controller.ts | 15 + src/modules/tenants/dashboard.service.ts | 63 + src/modules/tenants/dto/create-tenant.dto.ts | 62 + .../tenants/dto/gerar-faturas-mes.dto.ts | 8 + .../tenants/dto/list-faturas.query.dto.ts | 33 + .../tenants/dto/list-tenants.query.dto.ts | 28 + .../tenants/dto/update-tenant-status.dto.ts | 9 + src/modules/tenants/dto/update-tenant.dto.ts | 40 + src/modules/tenants/faturas.controller.ts | 54 + src/modules/tenants/faturas.service.ts | 177 + .../tenants/provisional-password.util.ts | 6 + src/modules/tenants/tenants.controller.ts | 64 + src/modules/tenants/tenants.module.ts | 14 + src/modules/tenants/tenants.service.ts | 223 + .../whatsapp/dto/testar-envio-whatsapp.dto.ts | 14 + src/modules/whatsapp/instance-name.util.ts | 51 + src/modules/whatsapp/whatsapp-status.util.ts | 16 + src/modules/whatsapp/whatsapp.controller.ts | 62 + src/modules/whatsapp/whatsapp.module.ts | 12 + src/modules/whatsapp/whatsapp.service.ts | 171 + test/auth.e2e-spec.ts | 440 + test/health.e2e-spec.ts | 34 + test/integracoes.e2e-spec.ts | 486 + test/jest-e2e.json | 9 + test/master.e2e-spec.ts | 551 + tsconfig.build.json | 4 + tsconfig.json | 22 + 118 files changed, 19173 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 Dockerfile create mode 100644 Etapas/00-README-ordem-das-etapas.md create mode 100644 Etapas/01-setup-projeto.md create mode 100644 Etapas/02-autenticacao-rls.md create mode 100644 Etapas/03-painel-master-tenants.md create mode 100644 Etapas/04-integracoes-evolution-email.md create mode 100644 Etapas/05-painel-tenant-customers-produtos.md create mode 100644 Etapas/06-vendas-dashboards.md create mode 100644 Etapas/07-motor-de-alertas.md create mode 100644 Etapas/08-personalizacao-whitelabel.md create mode 100644 Etapas/09-controle-de-progresso.md create mode 100644 Etapas/seconectai-vendas-arquitetura.md create mode 100644 Etapas/seconectai-vendas-schema.sql create mode 100644 README.md create mode 100644 docker-compose.override.yml create mode 100644 docker-compose.yml create mode 100644 eslint.config.mjs create mode 100644 nest-cli.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 prisma/migrations/20260812160945_init/migration.sql create mode 100644 prisma/migrations/20260812161100_rls_views_constraints/migration.sql create mode 100644 prisma/migrations/20260813120000_auth_app_roles/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 src/app.module.ts create mode 100644 src/common/audit-log.util.ts create mode 100644 src/common/encryption.module.ts create mode 100644 src/common/encryption.service.spec.ts create mode 100644 src/common/encryption.service.ts create mode 100644 src/common/mask.util.ts create mode 100644 src/config/env.validation.ts create mode 100644 src/config/prisma-master.service.ts create mode 100644 src/config/prisma-tenant.service.ts create mode 100644 src/config/prisma.module.ts create mode 100644 src/config/prisma.service.ts create mode 100644 src/config/tenant-context.service.ts create mode 100644 src/main.ts create mode 100644 src/modules/alert-dispatch/alert-dispatch.module.ts create mode 100644 src/modules/alert-dispatch/alert-dispatch.service.ts create mode 100644 src/modules/auth/auth.controller.ts create mode 100644 src/modules/auth/auth.module.ts create mode 100644 src/modules/auth/auth.service.ts create mode 100644 src/modules/auth/decorators/current-user.decorator.ts create mode 100644 src/modules/auth/decorators/roles.decorator.ts create mode 100644 src/modules/auth/dto/forgot-password.dto.ts create mode 100644 src/modules/auth/dto/login.dto.ts create mode 100644 src/modules/auth/dto/logout.dto.ts create mode 100644 src/modules/auth/dto/refresh.dto.ts create mode 100644 src/modules/auth/dto/reset-password.dto.ts create mode 100644 src/modules/auth/guards/jwt-auth.guard.ts create mode 100644 src/modules/auth/guards/master.guard.ts create mode 100644 src/modules/auth/guards/roles.guard.ts create mode 100644 src/modules/auth/guards/tenant.guard.ts create mode 100644 src/modules/auth/interceptors/tenant-rls.interceptor.ts create mode 100644 src/modules/auth/password.util.ts create mode 100644 src/modules/auth/strategies/jwt.strategy.ts create mode 100644 src/modules/auth/time.util.ts create mode 100644 src/modules/auth/token.util.ts create mode 100644 src/modules/auth/types/authenticated-request.type.ts create mode 100644 src/modules/auth/types/jwt-payload.type.ts create mode 100644 src/modules/email-config/dto/create-master-email-config.dto.ts create mode 100644 src/modules/email-config/dto/testar-envio-email.dto.ts create mode 100644 src/modules/email-config/dto/update-master-email-config.dto.ts create mode 100644 src/modules/email-config/dto/update-tenant-email-config.dto.ts create mode 100644 src/modules/email-config/email-config.module.ts create mode 100644 src/modules/email-config/email.module.ts create mode 100644 src/modules/email-config/email.service.ts create mode 100644 src/modules/email-config/master-email-config.controller.ts create mode 100644 src/modules/email-config/master-email-config.service.ts create mode 100644 src/modules/email-config/tenant-email-config.controller.ts create mode 100644 src/modules/email-config/tenant-email-config.service.ts create mode 100644 src/modules/evolution/dto/create-evolution-config.dto.ts create mode 100644 src/modules/evolution/dto/update-evolution-config.dto.ts create mode 100644 src/modules/evolution/evolution-api.client.ts create mode 100644 src/modules/evolution/evolution.controller.ts create mode 100644 src/modules/evolution/evolution.module.ts create mode 100644 src/modules/evolution/evolution.service.ts create mode 100644 src/modules/health/health.controller.spec.ts create mode 100644 src/modules/health/health.controller.ts create mode 100644 src/modules/health/health.module.ts create mode 100644 src/modules/health/health.service.ts create mode 100644 src/modules/planos/dto/create-plano.dto.ts create mode 100644 src/modules/planos/dto/list-planos.query.dto.ts create mode 100644 src/modules/planos/dto/update-plano.dto.ts create mode 100644 src/modules/planos/planos.controller.ts create mode 100644 src/modules/planos/planos.module.ts create mode 100644 src/modules/planos/planos.service.ts create mode 100644 src/modules/tenants/dashboard.controller.ts create mode 100644 src/modules/tenants/dashboard.service.ts create mode 100644 src/modules/tenants/dto/create-tenant.dto.ts create mode 100644 src/modules/tenants/dto/gerar-faturas-mes.dto.ts create mode 100644 src/modules/tenants/dto/list-faturas.query.dto.ts create mode 100644 src/modules/tenants/dto/list-tenants.query.dto.ts create mode 100644 src/modules/tenants/dto/update-tenant-status.dto.ts create mode 100644 src/modules/tenants/dto/update-tenant.dto.ts create mode 100644 src/modules/tenants/faturas.controller.ts create mode 100644 src/modules/tenants/faturas.service.ts create mode 100644 src/modules/tenants/provisional-password.util.ts create mode 100644 src/modules/tenants/tenants.controller.ts create mode 100644 src/modules/tenants/tenants.module.ts create mode 100644 src/modules/tenants/tenants.service.ts create mode 100644 src/modules/whatsapp/dto/testar-envio-whatsapp.dto.ts create mode 100644 src/modules/whatsapp/instance-name.util.ts create mode 100644 src/modules/whatsapp/whatsapp-status.util.ts create mode 100644 src/modules/whatsapp/whatsapp.controller.ts create mode 100644 src/modules/whatsapp/whatsapp.module.ts create mode 100644 src/modules/whatsapp/whatsapp.service.ts create mode 100644 test/auth.e2e-spec.ts create mode 100644 test/health.e2e-spec.ts create mode 100644 test/integracoes.e2e-spec.ts create mode 100644 test/jest-e2e.json create mode 100644 test/master.e2e-spec.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8196e1c --- /dev/null +++ b/.env.example @@ -0,0 +1,70 @@ +# ============================================================ +# SeConectai Vendas — variáveis de ambiente +# Copie este arquivo para `.env` e ajuste os valores. +# ============================================================ + +# Ambiente da aplicação: development | production | test +NODE_ENV=development + +# Porta HTTP exposta pela API (também usada pelo docker-compose para o mapeamento de porta) +PORT=3000 + +# --- Postgres -------------------------------------------------- +# Credenciais usadas pelo container do Postgres (docker-compose.yml) +POSTGRES_USER=seconectai +POSTGRES_PASSWORD=seconectai +POSTGRES_DB=seconectai_vendas +POSTGRES_PORT=5432 + +# String de conexão usada pelo Prisma. Dentro do docker-compose, o host é o +# nome do serviço (`postgres`); rodando a API fora do compose, use `localhost`. +DATABASE_URL=postgresql://seconectai:seconectai@postgres:5432/seconectai_vendas?schema=public + +# Connection string da role `app_master` (LOGIN + BYPASSRLS, criada na +# migration `20260813120000_auth_app_roles`), usada pelo painel master para +# enxergar dados de todos os tenants sem passar pelas policies de RLS. +DATABASE_URL_MASTER=postgresql://app_master:app_master_changeme@postgres:5432/seconectai_vendas?schema=public + +# Connection string da role `app_tenant` (LOGIN comum, sem BYPASSRLS), +# usada para toda query autenticada como tenant_user, dentro da transação +# que executa `SET LOCAL app.current_tenant_id` — é essa role que faz as +# policies de RLS valerem de fato (a role de DATABASE_URL é dona das +# tabelas e, por ser a superuser bootstrap do Postgres, sempre ignora RLS). +DATABASE_URL_TENANT=postgresql://app_tenant:app_tenant_changeme@postgres:5432/seconectai_vendas?schema=public + +# Em produção, troque as senhas acima com `ALTER ROLE app_master/app_tenant +# WITH PASSWORD '...'` e atualize as URLs de acordo — mesma lógica de +# POSTGRES_PASSWORD/JWT_SECRET: os valores default aqui são só para dev. + +# --- Redis ------------------------------------------------------- +REDIS_HOST=redis +REDIS_PORT=6379 + +# --- Autenticação -------------------------------------------------- +# Segredo usado para assinar os JWTs de acesso/refresh (mínimo 32 caracteres) +JWT_SECRET=troque-este-valor-por-um-segredo-forte-com-32-caracteres-ou-mais + +# Expiração do access token (formato aceito pela lib `ms`, ex: 15m, 1h) +JWT_ACCESS_EXPIRES_IN=15m + +# Expiração do refresh token e do token de reset de senha, em dias/horas +REFRESH_TOKEN_EXPIRES_IN_DAYS=30 +PASSWORD_RESET_EXPIRES_IN_HOURS=1 + +# Bloqueio de força bruta: nº de tentativas falhas e janela (minutos) até bloquear +LOGIN_MAX_ATTEMPTS=5 +LOGIN_LOCKOUT_WINDOW_MINUTES=15 + +# --- Painel Master ------------------------------------------------ +# Dias de trial concedidos a um tenant recém-criado (tenants.trial_expira_em) +TENANT_TRIAL_DIAS=14 + +# --- Integrações (Evolution API / SMTP) ---------------------------- +# Chave usada para cifrar segredos em repouso (AES-256-GCM): api_key da +# Evolution API (evolution_api_config.api_key_cripto) e senha SMTP +# (master_email_config.smtp_senha_cripto / tenant_email_config.smtp_senha_cripto). +# Mínimo 32 caracteres; internamente é reduzida a 32 bytes via SHA-256 antes +# de virar a chave AES-256, então qualquer string longa o suficiente serve. +# Nunca reutilize o valor de exemplo em produção — trocar invalida todos os +# segredos já cifrados no banco (seria necessário recriá-los). +ENCRYPTION_KEY=troque-este-valor-por-um-segredo-forte-com-32-caracteres-ou-mais diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95e94e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# dependencies +node_modules/ + +# build +dist/ +coverage/ + +# env +.env +.env.*.local + +# logs +*.log +npm-debug.log* + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store + +# uploads (etapa 8 — armazenamento local de logos) +uploads/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2fec1f2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100 +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..36116da --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +# syntax=docker/dockerfile:1 + +# ---- deps: instala todas as dependências (inclui dev, precisa pra build) ---- +FROM node:20-alpine AS deps +# python3/make/g++: precisos para compilar o binding nativo do `argon2` +# (não há prebuild musl/Alpine para todas as versões/arquiteturas). +RUN apk add --no-cache openssl python3 make g++ +WORKDIR /app +COPY package*.json ./ +COPY prisma ./prisma +RUN npm ci +RUN npx prisma generate + +# ---- dev: usado pelo docker-compose.override.yml, com hot-reload ---- +FROM deps AS dev +WORKDIR /app +COPY . . +CMD ["npm", "run", "dev"] + +# ---- build: compila TypeScript -> dist ---- +FROM node:20-alpine AS build +RUN apk add --no-cache openssl +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/prisma ./prisma +COPY . . +RUN npm run build + +# ---- prod-deps: reinstala só dependências de produção ---- +FROM node:20-alpine AS prod-deps +RUN apk add --no-cache openssl python3 make g++ +WORKDIR /app +COPY package*.json ./ +COPY prisma ./prisma +RUN npm ci --omit=dev +RUN npx prisma generate + +# ---- production: imagem final enxuta ---- +FROM node:20-alpine AS production +RUN apk add --no-cache openssl +WORKDIR /app +ENV NODE_ENV=production +COPY --from=prod-deps /app/node_modules ./node_modules +COPY --from=build /app/dist ./dist +COPY --from=build /app/prisma ./prisma +COPY package*.json ./ + +EXPOSE 3000 +CMD ["node", "dist/main"] diff --git a/Etapas/00-README-ordem-das-etapas.md b/Etapas/00-README-ordem-das-etapas.md new file mode 100644 index 0000000..9914801 --- /dev/null +++ b/Etapas/00-README-ordem-das-etapas.md @@ -0,0 +1,57 @@ +# SeConectai Vendas — Prompts de Build Faseado + +Este pacote contém 8 prompts sequenciais e autocontidos para entregar a uma +IA de geração de código (Claude Code, Cursor, Windsurf etc). Cada arquivo +pode ser colado como um prompt único — eles já trazem o contexto necessário +para não depender de memória de conversa anterior. + +## Ordem obrigatória + +1. `01-setup-projeto.md` — estrutura base, Docker Compose, Postgres, Prisma +2. `02-autenticacao-rls.md` — login master/tenant, JWT, RLS +3. `03-painel-master-tenants.md` — CRUD de tenants, planos, faturas +4. `04-integracoes-evolution-email.md` — Evolution API, instâncias WhatsApp, SMTP +5. `05-painel-tenant-customers-produtos.md` — CRUD de clientes e produtos/serviços +6. `06-vendas-dashboards.md` — vendas, parcelas, resumos +7. `07-motor-de-alertas.md` — BullMQ, templates, disparo e logs +8. `08-personalizacao-whitelabel.md` — logo/cor por tenant, ajustes finais + +## Arquivo de controle de progresso (obrigatório) + +`09-controle-de-progresso.md` é o estado oficial do projeto. Regras: + +- **Sempre envie esse arquivo junto com o prompt de cada etapa.** +- A IA deve lê-lo antes de começar e recusar iniciar uma etapa se a + anterior não estiver 100% concluída na checklist. +- Ao final de cada etapa, a IA deve devolver o arquivo atualizado (itens + marcados, status geral e observações preenchidas) — é esse arquivo + atualizado que você guarda e reenvia na próxima etapa/sessão. +- Sem esse arquivo, uma IA nova (em outra sessão) não tem como saber o + que já foi feito — por isso ele substitui a necessidade de repetir todo + o histórico da conversa. + +## Como usar cada prompt + +- Rode nessa ordem — cada etapa assume que a anterior está pronta e + funcionando (migrations aplicadas, testes passando). +- Ao final de cada etapa, confira o checklist de "Critérios de aceite" + antes de avançar para a próxima. Se algo não for atendido, peça correção + antes de seguir. +- O arquivo `seconectai-vendas-schema.sql` é a fonte de verdade do modelo + de dados — qualquer etapa que crie migration deve refletir exatamente + essas tabelas (a IA pode adaptar para sintaxe Prisma, mas a estrutura, + nomes de campo e tipos devem ser preservados). +- Cada prompt já repete o essencial do contexto (stack, convenções), então + eles funcionam mesmo em uma conversa nova/sessão nova com a IA. + +## Stack fixada em todas as etapas + +- Node.js 20+ / TypeScript +- NestJS (modular por domínio) +- PostgreSQL 16 + Prisma ORM +- Redis + BullMQ (a partir da etapa 7) +- Docker + Docker Compose +- Autenticação: JWT (access curto) + refresh token com rotação, senha com + argon2id +- Multi-tenancy: banco único, isolamento por `tenant_id` + Row Level + Security do Postgres diff --git a/Etapas/01-setup-projeto.md b/Etapas/01-setup-projeto.md new file mode 100644 index 0000000..99664f1 --- /dev/null +++ b/Etapas/01-setup-projeto.md @@ -0,0 +1,115 @@ +# Etapa 1/8 — Setup do Projeto + +## Contexto do projeto + +Estou construindo o **SeConectai Vendas**: um CRM/controle de vendas +multi-tenant. Existe um painel **master** (eu, dono da plataforma, que +vendo o acesso ao sistema) e painéis de **tenant** (meus clientes, que +usam o sistema para cadastrar os próprios clientes/vendas). + +Stack definida: +- Node.js 20+ / TypeScript +- NestJS (organizado em módulos por domínio) +- PostgreSQL 16 + Prisma ORM +- Redis (usado a partir de uma etapa futura, mas já deve subir no compose) +- Docker + Docker Compose para todo o ambiente (dev e produção) +- Multi-tenancy: banco único, isolamento por `tenant_id` + Row Level + Security (RLS) do Postgres + +Esta é a **Etapa 1 de 8** de um build faseado. Cada etapa será entregue +separadamente. Nesta etapa, quero **apenas a fundação do projeto** — +nenhuma feature de negócio ainda. + +## Objetivo desta etapa + +Criar a estrutura base do repositório, ambiente Docker funcional, conexão +com banco de dados via Prisma, e o schema inicial do banco já migrado. + +## Tarefas + +1. **Estrutura do projeto NestJS** + - Criar projeto NestJS em TypeScript com estrutura modular: + `src/modules//...` (os domínios virão nas próximas etapas — + por ora, criar só `src/modules/health/` com um endpoint + `GET /health` que retorna status da API e status da conexão com o + banco). + - Configurar ESLint + Prettier com regras razoáveis para TypeScript. + - Configurar variáveis de ambiente via `.env` (usar `@nestjs/config`), + com um `.env.example` documentando todas as chaves necessárias. + +2. **Docker Compose** + - Criar `docker-compose.yml` com os serviços: + - `app` (build a partir de um `Dockerfile` multi-stage — stage de + build e stage de produção enxuto) + - `postgres` (Postgres 16, volume nomeado para persistência, healthcheck) + - `redis` (Redis 7, volume nomeado, healthcheck) — não será usado + ainda, mas deixar disponível + - Criar `docker-compose.override.yml` (ou perfil `dev`) com hot-reload + do backend para desenvolvimento local. + - Garantir que `docker compose up` sobe tudo e a API responde em + `GET /health` com status 200. + +3. **Banco de dados e Prisma** + - Inicializar Prisma (`prisma/schema.prisma`) e traduzir **exatamente** + o schema SQL abaixo para o formato Prisma, preservando nomes de + tabelas/campos, tipos, enums, defaults, unique constraints e índices. + - Não implemente ainda a lógica de RLS na aplicação (isso é da Etapa + 2) — mas a migration gerada deve incluir os comandos + `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` e as `CREATE POLICY` + definidos no schema, via uma migration SQL raw do Prisma (Prisma não + suporta RLS nativamente, então usar `prisma migrate dev` seguido de + uma migration manual `.sql` para as policies). + - Rodar a migration inicial e confirmar que todas as tabelas, enums, + views e policies foram criadas corretamente no Postgres do + `docker-compose`. + + Schema de referência (colar aqui o conteúdo completo do arquivo + `seconectai-vendas-schema.sql` fornecido separadamente — use-o como + fonte de verdade). + +4. **Scripts utilitários** + - `package.json` com scripts: `dev`, `build`, `start:prod`, + `lint`, `test`, `prisma:migrate`, `prisma:generate`, `prisma:studio`. + - `README.md` inicial explicando como subir o projeto localmente + (`docker compose up`, variáveis de ambiente necessárias, como rodar + migrations). + +5. **Testes** + - Configurar Jest (já vem com NestJS) e criar um teste simples do + endpoint `/health`. + +## Estrutura de pastas esperada + +``` +seconectai-vendas/ +├── docker-compose.yml +├── docker-compose.override.yml +├── Dockerfile +├── .env.example +├── package.json +├── prisma/ +│ ├── schema.prisma +│ └── migrations/ +├── src/ +│ ├── main.ts +│ ├── app.module.ts +│ ├── config/ +│ └── modules/ +│ └── health/ +│ ├── health.module.ts +│ ├── health.controller.ts +│ └── health.service.ts +└── test/ +``` + +## Critérios de aceite + +- [ ] `docker compose up` sobe `app`, `postgres` e `redis` sem erro +- [ ] `GET /health` retorna 200 com status da API e status da conexão com + o banco +- [ ] Todas as tabelas, enums, views e índices do schema fornecido existem + no banco após a migration (conferir com `\dt` e `\dv` no psql) +- [ ] As policies de RLS estão criadas (`\d+ customers` mostra a policy) +- [ ] `.env.example` documenta todas as variáveis necessárias +- [ ] `npm run lint` e `npm run test` rodam sem erro +- [ ] README explica como subir o ambiente do zero diff --git a/Etapas/02-autenticacao-rls.md b/Etapas/02-autenticacao-rls.md new file mode 100644 index 0000000..3c37ae0 --- /dev/null +++ b/Etapas/02-autenticacao-rls.md @@ -0,0 +1,115 @@ +# Etapa 2/8 — Autenticação, Autorização e Row Level Security + +## Contexto do projeto + +Continuação do **SeConectai Vendas** (CRM/vendas multi-tenant). A Etapa 1 +já entregou: projeto NestJS + Docker Compose + Postgres/Prisma com o +schema completo migrado (incluindo as tabelas `master_admins`, +`tenant_users`, `auth_refresh_tokens`, `password_reset_tokens`, +`login_attempts` e as policies de RLS). + +Existem dois tipos de usuário: +- **master_admins**: você, dono da plataforma — acesso a todos os tenants. +- **tenant_users**: usuários de cada tenant (cliente seu), com papéis + (`tenant_admin`, `vendedor`, `financeiro`, `visualizador`), restritos + aos próprios dados via RLS. + +## Objetivo desta etapa + +Implementar login, emissão/renovação de tokens, reset de senha, +mecanismo de definição do `tenant_id` da sessão no Postgres (RLS) e +proteção contra força bruta — para ambos os tipos de usuário. + +## Tarefas + +1. **Módulo de autenticação (`src/modules/auth/`)** + - `POST /auth/master/login` — email + senha → valida contra + `master_admins.senha_hash` (argon2id) → retorna access token (JWT, + expiração curta, ex: 15 min) + refresh token (opaco, guardado como + hash em `auth_refresh_tokens`, expiração longa, ex: 30 dias). + - `POST /auth/tenant/login` — email + senha + (implícito) tenant + resolvido pelo email único dentro do tenant → mesma lógica, usando + `tenant_users`. + - `POST /auth/refresh` — recebe refresh token, valida hash e + expiração/revogação, **rotaciona** (invalida o antigo, emite um + novo) e retorna novo par de tokens. + - `POST /auth/logout` — revoga o refresh token atual. + - `POST /auth/password/forgot` — gera token em + `password_reset_tokens` (hash, expiração curta, ex: 1h) e dispara + e-mail (por ora, apenas logar o link/token no console — o envio real + de e-mail é de uma etapa futura). + - `POST /auth/password/reset` — valida token, atualiza `senha_hash`, + marca token como usado, revoga todos os refresh tokens ativos do + usuário. + +2. **JWT payload** + - Incluir no access token: `sub` (id do usuário), `tipo` + (`master`|`tenant_user`), `tenant_id` (null para master), `role` + (para tenant_user). + - Implementar `JwtStrategy` (Passport) validando assinatura e + expiração. + +3. **Guards e decorators** + - `@CurrentUser()` — decorator para extrair o usuário autenticado do + request. + - `MasterGuard` — permite apenas `tipo === 'master'`. + - `TenantGuard` — permite apenas `tipo === 'tenant_user'`, e injeta + `tenant_id` no contexto da request. + - `RolesGuard` + `@Roles('tenant_admin', ...)` — restringe endpoints + por papel dentro do tenant. + +4. **Aplicação do RLS via Prisma** + - Implementar um mecanismo (middleware/interceptor do Nest, executado + por request) que, para requests autenticadas como `tenant_user`, + abre uma transação Prisma e executa + `SET LOCAL app.current_tenant_id = ''` antes de qualquer + query — garantindo que a policy de RLS do Postgres filtre os dados + automaticamente. + - Para requests como `master`, usar uma conexão/role de banco com + `BYPASS ROW LEVEL SECURITY` (criar essa role na migration, se ainda + não existir, com `CREATE ROLE app_master BYPASSRLS LOGIN ...` — + documentar isso no `.env.example` como uma segunda connection string + opcional, `DATABASE_URL_MASTER`). + - Escrever um teste de integração que prove o isolamento: criar dois + tenants, um usuário em cada, confirmar que a query de um nunca + retorna dado do outro mesmo manipulando o `tenant_id` de outras + formas (ex: tentando passar um `tenant_id` diferente no body de uma + requisição — o `tenant_id` da sessão deve **sempre** vir do token, + nunca de input do usuário). + +5. **Proteção contra força bruta** + - Toda tentativa de login (sucesso ou falha) grava em + `login_attempts`. + - Após N tentativas falhas (configurável, ex: 5) para o mesmo e-mail + em uma janela de tempo (ex: 15 min), bloquear novas tentativas com + 429 e mensagem clara, mesmo que a senha esteja correta. + - Rate limiting geral no endpoint de login (ex: `@nestjs/throttler`). + +6. **Segurança de senha** + - Hash com argon2id (biblioteca `argon2`), parâmetros de custo + documentados no código. + - Validação de força mínima da senha no cadastro/reset (tamanho + mínimo, não permitir senhas triviais). + - Refresh tokens e reset tokens **nunca** armazenados em texto puro — + sempre hash (ex: sha256) antes de gravar no banco. + +## Critérios de aceite + +- [ ] Login master e login tenant funcionam e retornam access+refresh + token válidos +- [ ] Refresh token rotaciona corretamente; token antigo usado após + rotação é rejeitado +- [ ] Logout revoga o refresh token +- [ ] Fluxo de esqueci minha senha gera token, valida expiração/uso único +- [ ] `MasterGuard` e `TenantGuard` bloqueiam corretamente rotas erradas + (testar com token de tenant tentando acessar rota master e + vice-versa) +- [ ] `RolesGuard` bloqueia usuário `vendedor` tentando acessar rota + restrita a `tenant_admin` +- [ ] Teste de integração prova isolamento entre tenants via RLS (dado de + um tenant nunca vaza para outro, mesmo forçando `tenant_id` no + payload da requisição) +- [ ] Após 5 tentativas de login falhas, novas tentativas são bloqueadas + temporariamente +- [ ] Todas as senhas no banco estão como hash argon2id (nenhuma em texto + puro em nenhuma tabela) diff --git a/Etapas/03-painel-master-tenants.md b/Etapas/03-painel-master-tenants.md new file mode 100644 index 0000000..119125a --- /dev/null +++ b/Etapas/03-painel-master-tenants.md @@ -0,0 +1,98 @@ +# Etapa 3/8 — Painel Master: Tenants, Planos e Faturas + +## Contexto do projeto + +Continuação do **SeConectai Vendas**. Etapas anteriores entregaram: setup +do projeto (Docker/Prisma/schema) e autenticação completa (login +master/tenant, JWT, RLS, guards `MasterGuard`/`TenantGuard`/`RolesGuard`). + +Esta etapa implementa a parte do **painel master** relacionada à gestão +comercial da própria plataforma: cadastro dos tenants (seus clientes), +planos vendidos, e cobrança da mensalidade deles. + +Todos os endpoints desta etapa são protegidos por `MasterGuard`. + +## Objetivo desta etapa + +CRUD completo de `planos` e `tenants`, geração e controle de +`tenant_faturas` (mensalidades), e endpoints de consulta para o dashboard +do master (quantos tenants ativos, inadimplentes, faturamento previsto +etc). + +## Tarefas + +1. **Módulo `planos` (`src/modules/planos/`)** + - CRUD completo (`GET/POST/PUT/DELETE /master/planos`). + - Validação: `valor_mensal` > 0; limites (`limite_customers`, + `limite_usuarios`, `limite_instancias_whatsapp`) opcionais, mas se + informados devem ser > 0. + - Não permitir excluir plano em uso por algum tenant ativo — apenas + desativar (`ativo = false`). + +2. **Módulo `tenants` (`src/modules/tenants/`)** + - `POST /master/tenants` — cria tenant com status inicial `trial`, + associa a um plano, define `trial_expira_em` (ex: hoje + 14 dias, + configurável), e **cria automaticamente o primeiro usuário + `tenant_admin`** desse tenant (nome, e-mail e senha provisória + informados no payload, ou senha gerada e enviada por e-mail — por + ora, retornar a senha provisória na resposta, o envio de e-mail é de + etapa futura). + - `GET /master/tenants` — listagem com filtros por `status`, busca por + nome/documento, paginação. + - `GET /master/tenants/:id` — detalhe do tenant, incluindo plano, + últimas faturas, quantidade de usuários/customers/vendas + (agregados). + - `PUT /master/tenants/:id` — atualizar dados cadastrais, plano, + status. + - `PATCH /master/tenants/:id/status` — transição explícita de status + (`ativo`, `suspenso`, `cancelado`, `inadimplente`), registrando em + `audit_logs`. Regra: mudar para `suspenso` ou `cancelado` deve + também **bloquear login** dos `tenant_users` daquele tenant (checar + status do tenant no login e no `TenantGuard`). + +3. **Módulo `tenant_faturas` (dentro de `tenants` ou módulo próprio)** + - Geração automática mensal: um job (pode ser um endpoint manual por + enquanto, `POST /master/faturas/gerar-mes`, já preparando estrutura + para virar cron na Etapa 7) que cria, para cada tenant `ativo`, uma + linha em `tenant_faturas` referente ao mês corrente, com + `vencimento` calculado a partir de `dia_vencimento_mensalidade`, + evitando duplicar (respeitar o `UNIQUE(tenant_id, referencia)`). + - `GET /master/faturas` — listagem com filtros por status, tenant, + período. + - `PATCH /master/faturas/:id/marcar-pago` — marca fatura como paga + (`pago_em = now()`), e se o tenant estava `inadimplente`, volta para + `ativo`. + - Regra: se uma fatura passa do vencimento sem pagamento, marcar como + `atrasado` (pode ser endpoint manual por ora, `POST + /master/faturas/atualizar-atrasadas`, que também vira job na Etapa + 7) e mudar `tenants.status` para `inadimplente`. + +4. **Dashboard do master** + - `GET /master/dashboard` retornando: total de tenants por status, + MRR (soma de `valor_mensal` dos planos dos tenants ativos), total + recebido no mês corrente, total em aberto (`tenant_faturas` com + status `pendente` ou `atrasado`). + +5. **Auditoria** + - Toda criação/alteração de tenant, plano ou fatura grava uma linha em + `audit_logs` (`usuario_tipo='master'`, `acao`, `entidade`, + `entidade_id`, `detalhes` em JSON com o diff relevante). + +## Critérios de aceite + +- [ ] CRUD de planos funcional e validado +- [ ] Criar tenant também cria o primeiro `tenant_admin` funcional (login + testado) +- [ ] Listagem de tenants com filtro por status e busca funciona +- [ ] Mudar status do tenant para `suspenso`/`cancelado` bloqueia login + dos usuários daquele tenant (testar tentativa de login após + suspensão) +- [ ] Geração de faturas do mês não duplica fatura já existente para o + mesmo tenant/referência +- [ ] Marcar fatura como paga reverte tenant `inadimplente` → `ativo` +- [ ] Marcar fatura como atrasada muda tenant para `inadimplente` +- [ ] Dashboard retorna números corretos (conferir manualmente com dados + de teste) +- [ ] Todas as ações relevantes aparecem em `audit_logs` +- [ ] Todos os endpoints desta etapa são inacessíveis com token de + tenant_user (testar) diff --git a/Etapas/04-integracoes-evolution-email.md b/Etapas/04-integracoes-evolution-email.md new file mode 100644 index 0000000..c1be871 --- /dev/null +++ b/Etapas/04-integracoes-evolution-email.md @@ -0,0 +1,85 @@ +# Etapa 4/8 — Integrações: Evolution API (WhatsApp) e E-mail + +## Contexto do projeto + +Continuação do **SeConectai Vendas**. Já existem: setup do projeto, auth +completa com RLS, e o CRUD de tenants/planos/faturas no painel master. + +Esta etapa conecta a infraestrutura de mensageria: cadastro da Evolution +API (nível master), criação de instâncias de WhatsApp por tenant, e +configuração de SMTP (padrão da plataforma e por tenant). **O disparo +automático de alertas fica para a Etapa 7** — aqui o objetivo é deixar a +infraestrutura de envio pronta e testável manualmente. + +## Tarefas + +1. **Módulo `evolution` — nível master (`src/modules/evolution/`)** + - CRUD de `evolution_api_config` (`POST/GET/PUT/DELETE + /master/evolution-config`), protegido por `MasterGuard`. + - **Nunca** retornar `api_key_cripto` em texto puro nas respostas da + API — mascarar (ex: mostrar só os últimos 4 caracteres). + - Implementar criptografia AES-256-GCM para `api_key_cripto` (chave + vinda de variável de ambiente `ENCRYPTION_KEY`, nunca hardcoded). + Criar um `EncryptionService` reutilizável (vai servir também para + `smtp_senha_cripto`). + - `POST /master/evolution-config/:id/testar-conexao` — faz uma chamada + simples à Evolution API (ex: endpoint de status/versão) para + validar que a `base_url`/`api_key` cadastradas funcionam. + +2. **Módulo `whatsapp` — nível tenant** + - `POST /tenant/whatsapp/instancias` (`TenantGuard`, role + `tenant_admin`) — cria uma instância na Evolution API (nome único + sugerido automaticamente, ex: `tenant--`), grava + em `tenant_whatsapp_instances` com status `desconectado`. + - `GET /tenant/whatsapp/instancias/:id/qrcode` — solicita o QR code + de conexão à Evolution API e retorna para o front exibir; atualiza + `qr_code_gerado_em`. + - `GET /tenant/whatsapp/instancias/:id/status` — consulta status atual + na Evolution API e sincroniza com o campo `status` local + (desconectado/conectando/conectado/erro). + - `DELETE /tenant/whatsapp/instancias/:id` — desconecta e remove a + instância na Evolution API e localmente. + - `POST /tenant/whatsapp/instancias/:id/testar-envio` — envia uma + mensagem de teste para um número informado, útil para o tenant + validar a conexão antes de confiar no sistema. + - Respeitar `limite_instancias_whatsapp` do plano do tenant ao criar + nova instância (retornar erro claro se o limite foi atingido). + +3. **Módulo `email-config`** + - `master_email_config`: CRUD simples (`MasterGuard`) — geralmente só + um registro ativo, mas manter como tabela para permitir trocar + provedor sem downtime. + - `tenant_email_config`: `GET/PUT /tenant/email-config` + (`TenantGuard`, role `tenant_admin`) — se `usar_config_padrao=true`, + ocultar/ignorar os campos de SMTP próprio; se `false`, exigir os + campos de SMTP preenchidos. + - `POST /tenant/email-config/testar-envio` — envia e-mail de teste + para um endereço informado, usando a config resolvida (própria ou + padrão). + - Implementar `EmailService` central que resolve automaticamente qual + SMTP usar (do tenant ou o padrão) e usa `nodemailer`. + +4. **Serviço central de envio (preparando a Etapa 7)** + - Criar `AlertDispatchService` com dois métodos: + `enviarWhatsapp(tenantId, numero, mensagem)` e + `enviarEmail(tenantId, destinatario, assunto, corpo)`. Ambos apenas + executam o envio (sem lógica de "quando enviar" — isso é da Etapa + 7) e retornam sucesso/erro. Usar esse service nos endpoints de + "testar envio" acima, para já validar que funciona ponta a ponta. + +## Critérios de aceite + +- [ ] Master consegue cadastrar config da Evolution API e testar conexão +- [ ] `api_key` e senha SMTP nunca aparecem em texto puro em nenhuma + resposta de API (conferir payloads) +- [ ] Tenant consegue criar instância WhatsApp, obter QR code, escanear e + ver status mudar para `conectado` +- [ ] Criar instância além do limite do plano retorna erro claro +- [ ] Teste de envio de WhatsApp funciona (mensagem chega no número de + teste) +- [ ] Tenant consegue configurar SMTP próprio ou usar o padrão da + plataforma +- [ ] Teste de envio de e-mail funciona nos dois modos (SMTP próprio e + padrão) +- [ ] `EncryptionService` usado de forma consistente para todos os + segredos armazenados diff --git a/Etapas/05-painel-tenant-customers-produtos.md b/Etapas/05-painel-tenant-customers-produtos.md new file mode 100644 index 0000000..e2ec8a5 --- /dev/null +++ b/Etapas/05-painel-tenant-customers-produtos.md @@ -0,0 +1,69 @@ +# Etapa 5/8 — Painel Tenant: Clientes, Produtos e Serviços + +## Contexto do projeto + +Continuação do **SeConectai Vendas**. Já existem: setup, auth com RLS, +painel master (tenants/planos/faturas), e integrações de WhatsApp/e-mail. + +Esta etapa é o núcleo operacional do **painel do tenant**: cadastro dos +clientes dele (empresas que ele atende) e do catálogo de produtos e +serviços que ele vende. Todos os endpoints usam `TenantGuard` — o +`tenant_id` vem sempre do token, nunca de input do usuário. + +## Tarefas + +1. **Módulo `customers` (`src/modules/customers/`)** + - CRUD completo (`GET/POST/PUT/DELETE /tenant/customers`), com + `TenantGuard`. + - Campos obrigatórios: `razao_social`. Demais opcionais conforme + schema (`documento`, `email`, `telefone_whatsapp`, endereço etc). + - Validar `documento` como CPF ou CNPJ (formato, não precisa validar + dígito verificador na primeira versão — deixar comentário no código + indicando que pode evoluir). + - `GET /tenant/customers` — listagem com busca por nome/documento, + filtro por `ativo`, paginação. + - Respeitar `limite_customers` do plano do tenant (bloquear criação + além do limite, com mensagem clara). + - Soft delete: `DELETE` não remove a linha, apenas seta `ativo=false` + (histórico de vendas depende desses registros). + +2. **Submódulo `customer_contacts`** + - CRUD aninhado em `customers` (`/tenant/customers/:customerId/contatos`). + - Regra: apenas um contato pode ser `principal=true` por customer — + ao marcar um novo como principal, desmarcar o anterior + automaticamente (numa transação). + +3. **Módulo `products_services` (`src/modules/products/`)** + - CRUD completo (`GET/POST/PUT/DELETE /tenant/produtos`), com + `TenantGuard`. + - `tipo` obrigatório (`produto` ou `servico`); `valor_venda` + obrigatório e > 0. + - `sku` único por tenant quando informado (respeitar constraint do + banco, retornar erro 409 amigável em caso de conflito). + - `GET /tenant/produtos` — listagem com filtro por `tipo`, `ativo`, + busca por nome/sku. + - Soft delete igual a customers (`ativo=false`) — produto usado em + venda antiga não pode sumir do histórico. + +4. **Testes de RLS aplicados a esta etapa** + - Confirmar (teste automatizado) que um tenant não consegue, por + nenhum endpoint criado aqui, ler/editar/excluir customer ou produto + de outro tenant — mesmo sabendo o UUID exato do registro alheio + (deve retornar 404, não 403, para não vazar existência do recurso). + +## Critérios de aceite + +- [ ] CRUD de customers funcional, com soft delete e busca +- [ ] Criar customer além do `limite_customers` do plano retorna erro + claro +- [ ] CRUD de contatos aninhado funciona, e a regra de contato principal + único é respeitada +- [ ] CRUD de produtos/serviços funcional, com soft delete e busca +- [ ] SKU duplicado no mesmo tenant retorna erro 409 amigável; SKU igual + em tenants diferentes é permitido +- [ ] Teste automatizado confirma que tentar acessar customer/produto de + outro tenant retorna 404 +- [ ] Todos os endpoints desta etapa são inacessíveis com token master + "puro" sem contexto de tenant (ou documentar explicitamente se o + master deve conseguir acessar em modo de suporte — decisão a + confirmar antes de implementar) diff --git a/Etapas/06-vendas-dashboards.md b/Etapas/06-vendas-dashboards.md new file mode 100644 index 0000000..1599bc1 --- /dev/null +++ b/Etapas/06-vendas-dashboards.md @@ -0,0 +1,85 @@ +# Etapa 6/8 — Vendas, Parcelas e Dashboards + +## Contexto do projeto + +Continuação do **SeConectai Vendas**. Já existem: setup, auth com RLS, +painel master, integrações Evolution/e-mail, e CRUD de customers e +produtos/serviços no painel do tenant. + +Esta etapa implementa o fluxo de vendas propriamente dito: registrar uma +venda com itens, gerar parcelas a receber, e expor os dashboards de +acompanhamento (resumo semanal/mensal, total recebido, total a receber). + +## Tarefas + +1. **Módulo `sales` (`src/modules/sales/`)** + - `POST /tenant/vendas` — cria venda (`sales`) com seus itens + (`sale_items`) em uma única transação: + - Recebe `customer_id`, lista de itens + (`product_service_id`, `quantidade`, `valor_unitario` — permitir + sobrescrever o valor do catálogo caso tenha desconto pontual), + `desconto` geral, `observacoes`, `data_venda`. + - Calcula `valor_total` de cada item e da venda (soma dos itens + menos desconto). + - Recebe também a definição das parcelas (`sale_payments`): número + de parcelas, ou lista explícita de `{valor, vencimento}` — validar + que a soma das parcelas bate com `valor_total`. + - Validar que `customer_id` e todos os `product_service_id` + pertencem ao tenant autenticado (RLS já ajuda, mas validar + explicitamente para retornar erro de negócio claro em vez de erro + de banco). + - `GET /tenant/vendas` — listagem com filtros por `status`, + `customer_id`, período (`data_venda`), paginação. + - `GET /tenant/vendas/:id` — detalhe completo (itens + parcelas). + - `PATCH /tenant/vendas/:id/status` — transição de status + (`orcamento` → `confirmada` → `concluida`, ou `cancelada`). + Cancelar uma venda deve cancelar também as parcelas pendentes + (`sale_payments.status = 'cancelado'`). + - `PUT /tenant/vendas/:id` — editar venda em status `orcamento` + apenas (venda `confirmada` não deve ser editável em itens/valores — + só cancelada e recriada, para preservar histórico financeiro). + +2. **Módulo `sale_payments` (parcelas)** + - `GET /tenant/recebimentos` — listagem de parcelas com filtros por + `status`, período de vencimento, `customer_id`; usar a view + `vw_recebimentos` como base. + - `PATCH /tenant/recebimentos/:id/marcar-pago` — marca parcela como + paga (`pago_em = now()`, `forma_pagamento`). + - Endpoint (preparando Etapa 7): `POST + /tenant/recebimentos/atualizar-atrasadas` — marca como `atrasado` + toda parcela `pendente` com `vencimento` no passado (vira job + agendado na Etapa 7, aqui só o endpoint manual). + +3. **Dashboard do tenant** + - `GET /tenant/dashboard/resumo-semanal` e + `GET /tenant/dashboard/resumo-mensal` — usando as views + `vw_resumo_vendas_semanal` / `vw_resumo_vendas_mensal`, com filtro + de período opcional. + - `GET /tenant/dashboard/financeiro` — retorna: total recebido no mês + corrente, total a receber (pendente + atrasado), total atrasado + isolado (para destacar visualmente), próximas N parcelas a vencer. + +4. **Regras de consistência** + - Toda alteração relevante em `sales`/`sale_payments` grava em + `audit_logs` (`usuario_tipo='tenant_user'`, incluindo `usuario_id` e + `tenant_id`). + - Usar transações do Prisma em toda operação que mexe em mais de uma + tabela (venda + itens + parcelas) para evitar estado inconsistente. + +## Critérios de aceite + +- [ ] Criar venda com itens e parcelas funciona em uma única transação; + falha em qualquer parte reverte tudo +- [ ] Soma das parcelas é validada contra o valor total da venda +- [ ] Cancelar venda cancela as parcelas pendentes associadas +- [ ] Venda confirmada não pode ser editada em itens/valores (só via + cancelamento) +- [ ] Marcar parcela como paga reflete corretamente no dashboard + financeiro +- [ ] Dashboards semanal/mensal retornam números batendo com os dados de + teste inseridos manualmente +- [ ] Endpoint de atualizar atrasadas muda status corretamente das + parcelas vencidas +- [ ] Todas as queries usam RLS corretamente — venda de um tenant nunca + aparece para outro (reforçar teste de isolamento desta etapa + também) diff --git a/Etapas/07-motor-de-alertas.md b/Etapas/07-motor-de-alertas.md new file mode 100644 index 0000000..ac6380b --- /dev/null +++ b/Etapas/07-motor-de-alertas.md @@ -0,0 +1,89 @@ +# Etapa 7/8 — Motor de Alertas (BullMQ) + +## Contexto do projeto + +Continuação do **SeConectai Vendas**. Já existem: setup, auth com RLS, +painel master completo, integrações Evolution API/e-mail com serviço +central de envio (`AlertDispatchService`), CRUD de customers/produtos, e +o módulo completo de vendas/parcelas/dashboards. + +Esta é a etapa que **automatiza** o que até agora só existia como +endpoint manual: verificação de vencimentos e disparo de alertas, nos +dois níveis do sistema (master→tenant e tenant→customer). + +## Tarefas + +1. **Infraestrutura de filas** + - Configurar `@nestjs/bullmq` conectado ao Redis já disponível no + `docker-compose`. + - Criar duas filas: `alertas-mensalidade` (master→tenant) e + `alertas-cobranca` (tenant→customer). + - Criar um processo/worker separado do processo web principal (pode + ser o mesmo container em dev, mas documentar como se separaria em + produção — ex: `main.ts` vs `worker.ts`, ou um segundo serviço no + `docker-compose` chamado `worker` rodando o mesmo build com um + comando diferente). + +2. **Módulo `alert-templates`** + - CRUD de `alert_templates`: + - Nível master (`MasterGuard`): templates padrão da plataforma + (`tenant_id = null`), tipo `mensalidade_tenant`. + - Nível tenant (`TenantGuard`, role `tenant_admin`): templates + próprios, tipo `cobranca_cliente` (e outros tipos que fizerem + sentido, ex: `venda_confirmada`, `lembrete_vencimento`). Se o + tenant não tiver template próprio para um tipo/canal, usar o + template padrão do master como fallback. + - Implementar renderização de placeholders (`{{nome}}`, `{{valor}}`, + `{{vencimento}}`, etc — listar todos os placeholders suportados por + tipo de template em um arquivo de constantes, documentado). + +3. **Job diário: `mensalidade_tenant`** + - Cron (ex: todo dia às 8h, horário configurável via env) que: + 1. Roda a lógica equivalente ao endpoint + `POST /master/faturas/atualizar-atrasadas` (marca faturas + vencidas como `atrasado`, tenant vira `inadimplente`). + 2. Para toda `tenant_faturas` com vencimento em N dias (configurável, + ex: 3 dias antes) ou já vencida e ainda sem alerta hoje, enfileira + um job na fila `alertas-mensalidade` com os dados necessários. + - Worker da fila `alertas-mensalidade`: busca template + (`mensalidade_tenant`, canal `whatsapp` e/ou `email` conforme + configurado), renderiza, chama `AlertDispatchService`, grava + resultado em `alert_logs`, atualiza `tenant_faturas.alerta_enviado_em`. + +4. **Job diário: `cobranca_cliente`** (por tenant) + - Mesmo padrão do job acima, mas iterando `sale_payments` de todos os + tenants ativos: marca atrasadas, enfileira alerta para parcelas + próximas do vencimento ou já atrasadas (respeitando + `alerta_enviado_em` para não notificar duas vezes no mesmo dia). + - Usa a instância WhatsApp e config de e-mail **do tenant dono da + venda**, não a do master. + +5. **Idempotência e resiliência** + - Garantir que um mesmo job não dispare o mesmo alerta duas vezes + (checar `alerta_enviado_em` antes de enfileirar, e novamente antes + de enviar, dentro do worker, para lidar com concorrência). + - Configurar retry com backoff exponencial no BullMQ para falhas + transitórias de envio (ex: Evolution API fora do ar), com limite de + tentativas (ex: 3) e log do erro final em `alert_logs.erro`. + - Endpoint de observabilidade: `GET /master/alertas/logs` e + `GET /tenant/alertas/logs` para consultar `alert_logs` com filtros + (canal, status, período). + +## Critérios de aceite + +- [ ] Worker processa as duas filas de forma independente do processo + web (documentado como escalar workers separadamente) +- [ ] Template do tenant sobrepõe o padrão do master quando existe; usa + fallback quando não existe +- [ ] Placeholders são substituídos corretamente na renderização +- [ ] Job diário marca faturas/parcelas atrasadas corretamente +- [ ] Alerta de mensalidade chega no WhatsApp/e-mail do tenant em teste + controlado (vencimento simulado para "hoje") +- [ ] Alerta de cobrança chega no WhatsApp/e-mail do customer, usando a + instância/config do tenant correto (testar com 2 tenants + diferentes) +- [ ] Mesmo alerta não é disparado duas vezes no mesmo dia (testar + rodando o job duas vezes seguidas) +- [ ] Falha de envio é reprocessada com retry e, após esgotar tentativas, + fica registrada em `alert_logs` com erro +- [ ] Endpoints de consulta de logs funcionam com os filtros esperados diff --git a/Etapas/08-personalizacao-whitelabel.md b/Etapas/08-personalizacao-whitelabel.md new file mode 100644 index 0000000..2553a2a --- /dev/null +++ b/Etapas/08-personalizacao-whitelabel.md @@ -0,0 +1,87 @@ +# Etapa 8/8 — Personalização (Whitelabel) e Ajustes Finais + +## Contexto do projeto + +Etapa final do **SeConectai Vendas**. Todo o backend de negócio já está +pronto: auth com RLS, painel master (tenants/planos/faturas), integrações +WhatsApp/e-mail, customers/produtos, vendas/parcelas/dashboards, e o +motor de alertas automatizado. + +Esta etapa cobre a personalização visual por tenant (whitelabel) e uma +revisão geral de segurança/qualidade antes de considerar o backend pronto +para receber um frontend. + +## Tarefas + +1. **Upload e gestão de logo** + - `POST /tenant/perfil/logo` (`TenantGuard`, role `tenant_admin`) — + recebe upload de imagem (multipart), valida tipo (`png`/`jpg`/`svg`) + e tamanho máximo (ex: 2MB), armazena (definir estratégia: volume + Docker local para MVP, com estrutura pronta para trocar por S3/ + object storage depois — abstrair atrás de um `StorageService` com + interface clara) e atualiza `tenants.logo_url`. + - `DELETE /tenant/perfil/logo` — remove logo, volta ao padrão da + plataforma. + - `PUT /tenant/perfil/cor` — atualiza `tenants.cor_primaria` (validar + formato hex `#RRGGBB`). + - `GET /tenant/perfil` — retorna dados de personalização atuais + (logo_url, cor_primaria, nome_fantasia) para o frontend montar o + tema. + - Endpoint público (sem auth, ou com auth leve): `GET + /public/tenants/:slugOuId/tema` — para a tela de login do tenant já + carregar logo/cor antes do usuário autenticar (avaliar se precisa + de um campo `slug` único e amigável em `tenants`; se sim, criar + migration incremental para adicionar essa coluna). + +2. **Revisão de segurança geral** + - Checklist a validar/corrigir no código existente: + - Rate limiting está ativo em todos os endpoints de auth e nos + endpoints públicos. + - CORS configurado explicitamente (não `*`) — origem(ns) do + frontend via variável de ambiente. + - Helmet (`@nestjs/helmet` ou `helmet` direto) configurado para + headers de segurança padrão. + - Nenhum segredo (API keys, senhas SMTP, `ENCRYPTION_KEY`, + `JWT_SECRET`) hardcoded ou logado em texto puro em nenhum lugar do + código. + - Todos os inputs de API validados via DTOs com `class-validator` + (checar que nenhum endpoint aceita payload sem validação). + - Paginação obrigatória (com limite máximo) em todas as listagens, + para evitar consultas sem limite. + +3. **Documentação da API** + - Configurar Swagger/OpenAPI (`@nestjs/swagger`) documentando todos os + endpoints das 8 etapas, agrupados por módulo/tag, com exemplos de + request/response e indicação clara de quais exigem `MasterGuard` vs + `TenantGuard`. + +4. **Ambiente de produção** + - Revisar `Dockerfile` e `docker-compose.yml` para produção: + - Imagem final enxuta (multi-stage, sem devDependencies). + - Variáveis sensíveis via secrets/env, nunca commitadas. + - Healthchecks configurados em todos os serviços. + - Documentar no README o processo de deploy (build, migrations em + produção, rodar migration de policies RLS, subir worker + separadamente do web). + +5. **Testes finais de ponta a ponta** + - Escrever um teste E2E que cobre o fluxo completo mínimo: master + cria tenant → tenant faz login → cadastra customer e produto → faz + uma venda com parcelas → marca uma parcela como paga → confere + dashboard financeiro refletindo o pagamento. + +## Critérios de aceite + +- [ ] Upload/remoção de logo e alteração de cor funcionam e refletem em + `GET /tenant/perfil` +- [ ] Endpoint público de tema retorna logo/cor sem exigir autenticação +- [ ] CORS, Helmet e rate limiting revisados e ativos +- [ ] Nenhum segredo aparece em log ou resposta de API (revisão manual) +- [ ] Swagger disponível (ex: `/docs`) cobrindo todos os módulos, com + guards indicados +- [ ] Build de produção do Docker gera imagem sem dependências de + desenvolvimento +- [ ] Teste E2E do fluxo completo passa do início ao fim +- [ ] README final consolidado: como rodar em dev, como fazer deploy em + produção, como rodar migrations (incluindo policies de RLS) em um + ambiente novo diff --git a/Etapas/09-controle-de-progresso.md b/Etapas/09-controle-de-progresso.md new file mode 100644 index 0000000..8c91f32 --- /dev/null +++ b/Etapas/09-controle-de-progresso.md @@ -0,0 +1,601 @@ +# SeConectai Vendas — Controle de Progresso + +Este arquivo é o **estado oficial do projeto**. Ele deve ser mantido +atualizado pela IA que está construindo o sistema e consultado antes de +qualquer nova etapa começar. + +## Instruções obrigatórias para a IA + +1. **Antes de iniciar qualquer etapa**, peça este arquivo ao usuário (se + ele não for fornecido junto com o prompt da etapa) e leia a seção + "Status Geral" para confirmar que a etapa anterior está 100% concluída. +2. **Não inicie uma etapa** se a etapa anterior tiver qualquer item de + critério de aceite sem marcar. Se estiver incompleta, avise o usuário + e pergunte se deve terminar a etapa pendente antes de avançar. +3. **Ao concluir uma etapa**, marque com `[x]` cada item da checklist + correspondente (só marque o que foi de fato implementado e testado — + não marque por otimismo) e atualize: + - A seção "Status Geral" (etapa concluída, data) + - O campo "Observações" da etapa, se houver algo relevante para as + próximas etapas (ex: decisões tomadas, desvios do prompt original, + pendências técnicas deixadas para depois) +4. **Devolva este arquivo atualizado** ao usuário ao final de cada etapa, + junto com o restante da entrega, para que ele possa repassar na + próxima sessão/etapa. +5. Se o usuário pedir para pular uma etapa ou mudar a ordem, isso deve + ficar explicitamente registrado na seção "Observações" da etapa + afetada, com o motivo. + +## Status Geral + +| # | Etapa | Status | Concluída em | +|---|-------|--------|---------------| +| 1 | Setup do Projeto | Concluída | 2026-08-12 | +| 2 | Autenticação e RLS | Concluída | 2026-08-13 | +| 3 | Painel Master (tenants/planos/faturas) | Concluída | 2026-08-13 | +| 4 | Integrações Evolution API + E-mail | Concluída | 2026-08-14 | +| 5 | Painel Tenant (clientes/produtos) | [ ] Não iniciada | | +| 6 | Vendas, Parcelas e Dashboards | [ ] Não iniciada | | +| 7 | Motor de Alertas (BullMQ) | [ ] Não iniciada | | +| 8 | Personalização Whitelabel e Ajustes Finais | [ ] Não iniciada | | + +Valores possíveis para "Status": `Não iniciada`, `Em andamento`, +`Concluída`, `Concluída com pendências` (usar este último se algo ficou +propositalmente para depois — detalhar em Observações). + +--- + +## Etapa 1 — Setup do Projeto + +- [x] `docker compose up` sobe `app`, `postgres` e `redis` sem erro +- [x] `GET /health` retorna 200 com status da API e status da conexão com o banco +- [x] Todas as tabelas, enums, views e índices do schema fornecido existem no banco após a migration +- [x] As policies de RLS estão criadas +- [x] `.env.example` documenta todas as variáveis necessárias +- [x] `npm run lint` e `npm run test` rodam sem erro +- [x] README explica como subir o ambiente do zero + +**Observações:** + +- Estrutura NestJS, `ConfigModule`/validação de env, `PrismaService`/`PrismaModule` + e o módulo `health` (controller + service + spec) já existiam de uma sessão + anterior; esta sessão completou o restante da etapa: `Dockerfile` multi-stage + (stages `deps`, `dev`, `build`, `prod-deps`, `production`), `docker-compose.yml`, + `docker-compose.override.yml` (hot-reload dev), `.env.example`, `README.md` e as + migrations do Prisma. +- `prisma/schema.prisma` já era a tradução 1:1 do schema SQL fornecido. Construtos + que o Prisma DSL não expressa nativamente (views de dashboard, `CHECK` constraint + de `dia_vencimento_mensalidade`, índices parciais e as `CREATE POLICY` de RLS) + foram implementados em uma migration manual separada + (`prisma/migrations/20260812161100_rls_views_constraints/`), aplicada com + `prisma migrate deploy` logo após a migration inicial gerada automaticamente + (`prisma migrate dev --name init`). Confirmado via `\dt`, `\dv`, `\d+ tenants` e + `\d+ customers` no psql: 22 tabelas, 6 enums, 3 views, o `CHECK` e as 9 policies + de isolamento por tenant. +- As imagens Alpine (`node:20-alpine`) precisaram de `apk add openssl` em todos os + estágios que rodam `prisma generate`/`node`, pois o Prisma não detecta + libssl/openssl no Alpine puro e cai num engine binário incompatível (aviso + "Prisma failed to detect the libssl/openssl version"). +- `@types/supertest` estava fixado em `^6.0.2` no `package.json`, mas o `npm + install` resolveu `supertest@7.2.2` (com `superagent@10.x` como dependência) — + uma combinação incompatível com os tipos da v6, quebrando `npm run lint` com + erros de "Unsafe call"/"not callable" em `test/health.e2e-spec.ts`. Corrigido + atualizando para `@types/supertest@^7.0.3` e trocando `import * as request from + 'supertest'` por `import request, { Response } from 'supertest'` (a v7 dos tipos + exige import default por causa da interop com `esModuleInterop`). +- `package-lock.json` foi gerado nesta sessão (não existia antes); `npm ci` no + Dockerfile depende dele. +- Ambiente de execução não tinha Node/npm no host, e os containers Docker deste + sandbox não têm acesso de rede via bridge padrão (bloqueado no nível do host) — + só o modo `--network host` alcança a internet. Isso só afetou os comandos usados + para *verificar* a etapa nesta sessão (`docker build --network host`, `docker + run --network host ... npm install`); `Dockerfile`/`docker-compose.yml` + entregues não dependem disso e devem funcionar normalmente em qualquer ambiente + Docker com rede padrão. + +--- + +## Etapa 2 — Autenticação, Autorização e Row Level Security + +- [x] Login master e login tenant funcionam e retornam access+refresh token válidos +- [x] Refresh token rotaciona corretamente; token antigo rejeitado após rotação +- [x] Logout revoga o refresh token +- [x] Fluxo de esqueci minha senha gera token, valida expiração/uso único +- [x] `MasterGuard` e `TenantGuard` bloqueiam corretamente rotas erradas +- [x] `RolesGuard` bloqueia usuário sem permissão de papel +- [x] Teste de integração prova isolamento entre tenants via RLS +- [x] Bloqueio temporário após tentativas de login falhas +- [x] Todas as senhas no banco estão como hash argon2id + +**Observações:** + +- Módulo `src/modules/auth/` completo: `AuthController`/`AuthService` + (`master/login`, `tenant/login`, `refresh`, `logout`, `password/forgot`, + `password/reset`), `JwtStrategy` (passport-jwt), guards (`JwtAuthGuard`, + `MasterGuard`, `TenantGuard`, `RolesGuard`), decorators (`@CurrentUser()`, + `@Roles(...)`), `password.util.ts` (argon2id, parâmetros OWASP 2024: + memoryCost 19456/timeCost 2/parallelism 1, + validação de força mínima) e + `token.util.ts` (refresh/reset tokens opacos, guardados só como hash + sha256 — nunca em texto puro). +- **Desvio do prompt original, registrado em detalhe no histórico abaixo:** + o prompt só previa uma segunda connection string (`DATABASE_URL_MASTER`, + role `app_master BYPASSRLS`) para o master. Foi necessário criar também + uma terceira role/connection string (`app_tenant`/`DATABASE_URL_TENANT`) + para que o RLS realmente filtre as queries de `tenant_user` — a role de + `DATABASE_URL` (`seconectai`) é a superuser bootstrap do Postgres + (criada pelo `POSTGRES_USER` do docker-compose desde a Etapa 1), e + superusers sempre ignoram RLS independente das policies existirem. + Ambas as roles são criadas na migration manual + `prisma/migrations/20260813120000_auth_app_roles/`. +- Mecanismo de RLS por request: `TenantRlsInterceptor` (registrado + globalmente via `APP_INTERCEPTOR`) detecta requests autenticadas como + `tenant_user` e, através de `TenantContextService`, abre uma transação + Prisma na conexão `app_tenant` executando + `SELECT set_config('app.current_tenant_id', '', true)` + (equivalente a `SET LOCAL`, mas parametrizável — evita interpolar string + na query). O `tenant_id` usado vem **sempre** do JWT da sessão + (`request.user.tenant_id`, populado pelo `JwtStrategy`/`TenantGuard`), + nunca de body/query — verificado no teste de isolamento (endpoint + `GET /auth/tenant/users` ignora um `?tenant_id=` forçado + na query string). +- Requests master usam `PrismaMasterService` (conexão `app_master`, + bypassa RLS) — não passam pelo `TenantRlsInterceptor`. +- Endpoints de demonstração/utilidade adicionados para exercitar os guards + e o RLS de ponta a ponta (não fazem parte do prompt original, mas são + necessários para provar os critérios de aceite via HTTP real, e ficam + como funcionalidade útil para as próximas etapas): `GET /auth/me` + (qualquer usuário autenticado), `GET /auth/master/admins` (só master), + `GET /auth/tenant/users` (só `tenant_admin`, lista usuários do próprio + tenant via RLS). +- `email` de `tenant_users` é único só por tenant (`UNIQUE(tenant_id, + email)`, não globalmente) — login/forgot-password por e-mail, sem mais + contexto, não conseguem resolver com segurança um e-mail que colida + entre dois tenants distintos. Decisão: nesse caso (0 ou >1 tenant_user + com o e-mail informado), tratar como credencial inválida em vez de + arriscar autenticar na conta errada (`AuthService.uniqueTenantUserByEmail`). + Fica registrado como limitação conhecida para uma etapa futura resolver + (ex: login por subdomínio/slug do tenant). +- `POST /auth/password/forgot` sempre responde com a mesma mensagem + genérica, exista ou não a conta — prática padrão para não vazar quais + e-mails estão cadastrados (não pedido explicitamente no prompt, mas não + conflita com nenhum critério de aceite). +- Reuso de um refresh token já rotacionado (revogado) é tratado como + indício de roubo de token: além de rejeitar a tentativa, revoga toda a + família de tokens ativos daquele usuário (defesa em profundidade, além + do pedido mínimo do prompt). +- `argon2` é um módulo nativo (binding C++) sem prebuild musl/Alpine para + esta versão — precisou adicionar `python3 make g++` aos estágios `deps` + e `prod-deps` do `Dockerfile` (ausentes na imagem final `production`, + só usados para compilar durante o build). +- Teste e2e `test/auth.e2e-spec.ts` cobre todos os critérios de aceite + contra Postgres real (via `docker compose`): login (master/tenant, + sucesso/falha), rotação de refresh token (incluindo o cenário de reuso + de token revogado revogando a família toda), logout, forgot/reset + (token único-uso, expiração, senha fraca rejeitada), os quatro guards + (`JwtAuthGuard`/`MasterGuard`/`TenantGuard`/`RolesGuard`) nos dois + sentidos, isolamento por RLS (dois tenants reais, tentativa de forçar + `tenant_id` de outro tenant via query string) e bloqueio por força + bruta (5 tentativas falhas → 429 mesmo com senha correta na 6ª). + Rodado com `docker compose exec app npx prisma migrate deploy` seguido + de `npx jest --config ./test/jest-e2e.json` — 21/21 testes passando. + `npm run lint`, `tsc --noEmit` e `npm run build` (produção) também + passam sem erros; smoke test manual via `curl` contra a imagem + `production` real confirmou login + guards funcionando fim a fim. + +--- + +## Etapa 3 — Painel Master: Tenants, Planos e Faturas + +- [x] CRUD de planos funcional e validado +- [x] Criar tenant também cria o primeiro `tenant_admin` funcional +- [x] Listagem de tenants com filtro por status e busca funciona +- [x] Suspender/cancelar tenant bloqueia login dos usuários dele +- [x] Geração de faturas do mês não duplica fatura existente +- [x] Marcar fatura como paga reverte tenant `inadimplente` → `ativo` +- [x] Marcar fatura como atrasada muda tenant para `inadimplente` +- [x] Dashboard master retorna números corretos +- [x] Ações relevantes aparecem em `audit_logs` +- [x] Endpoints inacessíveis com token de tenant_user + +**Observações:** + +- Módulo `src/modules/planos/` (`PlanosController`/`PlanosService`, CRUD completo em + `/master/planos`) e módulo `src/modules/tenants/` (`TenantsController`/`TenantsService` + em `/master/tenants`, `FaturasController`/`FaturasService` em `/master/faturas`, + `DashboardController`/`DashboardService` em `/master/dashboard`, todos agrupados no + mesmo `TenantsModule`). Todos os controllers desta etapa usam + `@UseGuards(JwtAuthGuard, MasterGuard)` a nível de classe e a conexão `PrismaMasterService` + (`app_master`, BYPASSRLS) — `tenants`/`planos`/`tenant_faturas`/`audit_logs` não têm RLS + habilitado (só tabelas por-tenant têm), mas a conexão master é usada por consistência + arquitetural com a Etapa 2. +- Auditoria centralizada em `src/common/audit-log.util.ts` (`writeAuditLog(client, params)`, + função simples — não um serviço injetável — para poder receber tanto `PrismaMasterService` + quanto o `tx` de uma transação em andamento, seguindo o mesmo padrão de utilitário puro já + usado em `auth/password.util.ts`/`token.util.ts`). Toda operação de escrita desta etapa + (criar/atualizar plano, excluir/desativar plano, criar tenant, atualizar tenant, mudar + status, gerar faturas do mês, marcar fatura paga, atualizar atrasadas) grava a auditoria + **dentro da mesma transação** da operação principal, para que auditoria e efeito nunca + fiquem dessincronizados. +- **Desvio do prompt original:** o prompt lista `status` como um dos campos que o `PUT + /master/tenants/:id` atualiza. Implementado com `status` **fora** do `UpdateTenantDto` — + toda transição de status passa exclusivamente por `PATCH /master/tenants/:id/status` + (`TenantsService.changeStatus`), que é o único caminho que grava a auditoria da mudança + (`de`/`para`) e aplica o bloqueio de acesso. Deixar `status` editável também via PUT + abriria um caminho para mudar o status sem passar por essa lógica. +- **Desvio/extensão do prompt original, relevante para segurança:** o prompt pedia bloquear + login após suspender/cancelar um tenant ("checar status do tenant no login e no + `TenantGuard`"). Login já checava (Etapa 2); nesta etapa o `TenantGuard` + (`auth/guards/tenant.guard.ts`) passou a consultar o status do tenant a cada request + (antes só populava `request.tenant_id`, sem checar nada) — um access token emitido antes + da suspensão continua criptograficamente válido até expirar (15 min por padrão), então sem + essa checagem o usuário suspenso continuaria acessando a API normalmente até o token + expirar. Isso adiciona uma consulta a `tenants` por request autenticada como tenant_user + (não cacheada por ora — aceitável no volume atual, pode virar ponto de otimização numa + etapa futura). Como defesa em profundidade adicional (mesmo espírito da revogação de + família de refresh tokens da Etapa 2), `changeStatus` também revoga todos os refresh + tokens ativos dos `tenant_users` do tenant ao suspender/cancelar. +- `TENANT_BLOCKED_STATUSES` (antes uma constante privada em `auth.service.ts`) foi exportado + para ser a única fonte de verdade de quais status bloqueiam acesso, reusada tanto no login + (Etapa 2) quanto no `TenantGuard` (Etapa 3) — evita as duas listas divergirem no futuro. +- Exclusão de plano (`DELETE /master/planos/:id`): interpretado "em uso por algum tenant + ativo" como qualquer tenant com `plano_id` apontando pra ele e `status != 'cancelado'` + (não só `status = 'ativo'`) — um tenant `trial`/`inadimplente`/`suspenso` também depende do + plano estar íntegro. Nesse caso a exclusão é convertida em desativação (`ativo = false`, + plano continua existindo e servindo os tenants já associados) e a resposta indica + `{deletado:false, desativado:true}`; sem tenants associados (exceto cancelados), a linha é + excluída de fato (`{deletado:true, desativado:false}`). Um plano desativado não pode ser + associado a tenants novos (`create`/`update` de tenant checam `plano.ativo`), mas tenants + já associados a ele continuam funcionando normalmente (não há "downgrade forçado"). +- Senha provisória do primeiro `tenant_admin`: se o payload de `POST /master/tenants` não + informar `admin.senha`, uma é gerada (`tenants/provisional-password.util.ts`, + `randomBytes(9).toString('base64url')`) e devolvida em texto puro **só na resposta desse + request** (nunca fica logada nem persistida em texto puro — só o hash argon2id via + `password.util.ts`, igual ao resto do sistema). Envio por e-mail é a Etapa 4 + (Integrações), como o prompt já previa. +- `POST /master/faturas/gerar-mes` usa `createMany({ skipDuplicates: true })` em vez de + checar linha a linha se já existe fatura pro `(tenant_id, referencia)` — o + `UNIQUE(tenant_id, referencia)` do schema garante a idempotência a nível de banco; chamar + de novo no mesmo mês (ou re-chamar manualmente antes da automação virar cron na Etapa 7) é + seguro. +- `POST /master/faturas/atualizar-atrasadas` só rebaixa pra `inadimplente` tenants que + estejam `ativo` no momento — tenants já `suspenso`/`cancelado` com fatura vencida viram + `atrasado` na fatura, mas não voltam a virar `inadimplente` (não faria sentido reativar + parcialmente um tenant cancelado só por causa de uma fatura antiga). +- Dashboard (`GET /master/dashboard`): `mrr` calculado somando `planos.valor_mensal` dos + tenants `ativo` em memória com `Prisma.Decimal` (não dá pra fazer um `groupBy`/`aggregate` + do Prisma somando um campo de tabela relacionada em uma única query) — evita o erro de + arredondamento de ponto flutuante que uma soma em `number` teria com valores monetários. + `total_recebido_mes`/`total_em_aberto` usam `aggregate({_sum})` diretamente. Todos os + valores voltam formatados com `.toFixed(2)`. +- Variável de ambiente nova: `TENANT_TRIAL_DIAS` (default 14, dias de trial concedidos na + criação do tenant), adicionada a `env.validation.ts` e `.env.example`, mesmo padrão das + demais variáveis opcionais com default. +- `package.json`: `test:e2e` passou a rodar com `--runInBand`. Motivo: a partir desta etapa + os testes e2e leem estado **global** do painel master (contagem de tenants por status, + MRR, faturas do mês corrente) — com múltiplos arquivos de teste (`auth.e2e-spec.ts` e o + novo `master.e2e-spec.ts`) rodando em paralelo (padrão do Jest), um teste que cria um + tenant `ativo` num arquivo poderia contaminar uma asserção de contagem global no outro. + `--runInBand` roda os arquivos em série, eliminando essa fonte de flakiness — mesmo assim, + as asserções mais sensíveis (dashboard) comparam o valor antes/depois da operação testada + (delta), em vez do valor absoluto, para não depender de nenhum estado pré-existente no + banco. +- Teste e2e novo, `test/master.e2e-spec.ts`, cobrindo todos os critérios de aceite: CRUD de + planos (validação de `valor_mensal`/limites, exclusão bloqueada por tenant em uso vs. + exclusão real quando não usado), criação de tenant com login do `tenant_admin` gerado + testado de ponta a ponta, listagem com filtro por `status`/busca, mudança de status + bloqueando tanto login novo quanto um token de acesso já emitido (via `TenantGuard`), + geração de faturas idempotente, marcar-pago revertendo `inadimplente`→`ativo` (incluindo + rejeição 409 ao tentar marcar pago de novo), atualizar-atrasadas propagando pra + `tenants.status`, dashboard (com asserções por delta) e bloqueio de todos os grupos de + endpoint com token de `tenant_user`. Rodado com `docker compose exec`-equivalente (`docker + run` num container a partir da imagem `dev` do `Dockerfile`, ligado à mesma rede do + `docker-compose.yml`, já que a imagem `dev` compila os bindings nativos do `argon2` pra + musl/Alpine — o host deste ambiente é glibc e não tem acesso de rede pra build de imagem + sem `--network host`) — `npx prisma migrate status` confirmou que as migrations da Etapa 1 + já cobrem todo o schema desta etapa (`planos`/`tenants`/`tenant_faturas`/`audit_logs` já + existiam), então não houve migration nova. `npm run lint`, `npx tsc --noEmit`, `npm run + test` (unit) e `npm run build` passam sem erros; `npx jest --config ./test/jest-e2e.json + --runInBand` — 43/43 testes passando (os 21 da Etapa 2 + 1 do health + 21 novos desta + etapa). Build de produção (`docker build --target production`) também verificado, com + smoke test manual via `curl` confirmando `/health` (200) e bloqueio de `/master/planos` + sem token (401) contra a imagem `production` real. + +--- + +## Etapa 4 — Integrações: Evolution API e E-mail + +- [x] Master cadastra config da Evolution API e testa conexão +- [x] `api_key`/senha SMTP nunca aparecem em texto puro nas respostas +- [x] Tenant cria instância WhatsApp, obtém QR code, conecta +- [x] Criar instância além do limite do plano retorna erro claro +- [x] Teste de envio de WhatsApp funciona +- [x] Tenant configura SMTP próprio ou usa o padrão da plataforma +- [x] Teste de envio de e-mail funciona nos dois modos +- [x] `EncryptionService` usado de forma consistente para segredos + +**Observações:** + +- Módulos novos: `src/modules/evolution/` (`EvolutionController`/`EvolutionService`/`EvolutionApiClient`, + CRUD de `evolution_api_config` em `/master/evolution-config` + `testar-conexao`), + `src/modules/whatsapp/` (`WhatsappController`/`WhatsappService`, instâncias em + `/tenant/whatsapp/instancias`), `src/modules/email-config/` (`MasterEmailConfigController`/`Service` + em `/master/email-config`, `TenantEmailConfigController`/`Service` em `/tenant/email-config`, + `EmailService` com `nodemailer`) e `src/modules/alert-dispatch/` (`AlertDispatchService`, preparando a + Etapa 7). Todos os controllers master usam `@UseGuards(JwtAuthGuard, MasterGuard)` + + `PrismaMasterService`, mesmo padrão da Etapa 3; os controllers tenant usam + `@UseGuards(JwtAuthGuard, TenantGuard, RolesGuard)` + `@Roles('tenant_admin')`. +- `EncryptionService` novo em `src/common/encryption.service.ts` (AES-256-GCM, `node:crypto` nativo, sem + dependência extra), registrado em `EncryptionModule` (`@Global()`, mesmo padrão do `PrismaModule`). + Formato gravado nas colunas `*_cripto`: `iv:authTag:conteudo` (hex, `:`-separado). A chave de + `ENCRYPTION_KEY` (env, nova variável obrigatória, `@MinLength(32)` em `env.validation.ts`, mesmo padrão + de `JWT_SECRET`) é reduzida a 32 bytes via SHA-256 antes de virar a chave AES-256 — assim qualquer + string longa o suficiente serve, sem exigir um valor hex/base64 de tamanho exato. Mascaramento para + exibição (`src/common/mask.util.ts`, `maskSecret`) sempre usa largura fixa de asteriscos + últimos 4 + caracteres do valor decifrado — nunca revela o tamanho real do segredo. `api_key_cripto`/ + `smtp_senha_cripto` são explicitamente excluídos do objeto de resposta (destructuring) em todo + endpoint que retorna essas entidades — conferido em teste e2e (`api_key_cripto`/`smtp_senha_cripto` + sempre `undefined` no payload). +- **Decisão de arquitetura, para registrar com destaque:** `AlertDispatchService` (e, por baixo dele, + `EmailService`) usam `PrismaMasterService` com filtro manual de `tenant_id` em toda query, em vez de + `TenantContextService`/RLS — diferente do resto do código tenant desta etapa + (`WhatsappService`/`TenantEmailConfigService`, que usam `tenantContext.getTx()` normalmente, já que + `tenant_whatsapp_instances`/`tenant_email_config` têm RLS habilitado desde a migration da Etapa 2). + Motivo: `AlertDispatchService` é explicitamente "o serviço central de envio, preparando a Etapa 7" — o + worker BullMQ da Etapa 7 vai chamá-lo fora de qualquer request HTTP, sem JWT/tenant autenticado e, + portanto, sem nenhum contexto de `AsyncLocalStorage` ativo (`TenantContextService.getTx()` lançaria + "chamado fora de um contexto de tenant ativo"). Usar `PrismaMasterService` desde já evita ter que + reescrever esse service na Etapa 7. Como esse service nunca recebe `tenant_id` de input do usuário + final (sempre do JWT via controller, e futuramente do job da fila), o filtro manual é seguro, mas fica + registrado aqui porque é a única exceção ao padrão RLS-first do resto do projeto. +- **Decisão de wiring do Nest, sem impacto de comportamento:** `EmailService` (o `nodemailer` + propriamente dito) vive num módulo próprio, `EmailModule` (`src/modules/email-config/email.module.ts`), + separado do `EmailConfigModule` (CRUD). Motivo: `AlertDispatchModule` precisa do `EmailService`, e + `EmailConfigModule` precisa do `AlertDispatchService` (endpoint `testar-envio` do tenant) — um único + módulo cobrindo as duas coisas criaria um ciclo de imports (`EmailConfigModule` → `AlertDispatchModule` + → `EmailConfigModule`). Grafo final, sem ciclos: `EvolutionModule`/`EmailModule` → `AlertDispatchModule` + → `WhatsappModule`/`EmailConfigModule`. +- **Desvio do prompt original, pequeno e documentado:** `AlertDispatchService.enviarWhatsapp` recebeu um + quarto parâmetro opcional, `instanceId?: string` (assinatura do prompt previa só + `enviarWhatsapp(tenantId, numero, mensagem)`). Sem `instanceId`, resolve a instância `conectado` mais + recente do tenant (comportamento que a Etapa 7 vai usar). Com `instanceId`, envia especificamente por + aquela instância — usado por `POST /tenant/whatsapp/instancias/:id/testar-envio`, que por natureza + precisa testar uma instância específica (não "uma instância qualquer do tenant"), mantendo ainda assim + o pedido do prompt de "usar esse service nos endpoints de testar envio". +- `EvolutionApiClient` (`src/modules/evolution/evolution-api.client.ts`) implementado contra a + especificação pública REST da Evolution API v2 (`apikey` no header, `/instance/create`, + `/instance/connect/:name`, `/instance/connectionState/:name`, `/instance/logout|delete/:name`, + `/message/sendText/:name`) — mesma versão (`2.3`) já assumida como default no schema + (`evolution_api_config.versao`). **Não há uma instância real da Evolution API disponível neste ambiente + de desenvolvimento/CI** (sem internet dedicada a isso nem servidor de referência) para validar contra o + serviço de verdade — mitigado escrevendo, no teste e2e, um servidor HTTP real e local + (`FakeEvolutionServer`, `node:http` puro, sem dependência nova) que implementa o mesmo contrato e roda + na porta efêmera do próprio processo de teste, exercitando o `EvolutionApiClient` real ponta a ponta + (não mockado) contra um servidor de verdade. Fica como pendência natural confirmar contra uma instância + real da Evolution API antes de operar em produção — mesmo espírito da limitação de rede Docker já + registrada na Etapa 1. +- `tenant_whatsapp_instances.status` é `String` livre no schema (não enum do Prisma) — validado/mapeado a + nível de aplicação em `src/modules/whatsapp/whatsapp-status.util.ts` + (`WhatsappInstanceStatus = 'desconectado' | 'conectando' | 'conectado' | 'erro'`, + `mapEvolutionState()` traduz o `state` da Evolution — `open`/`connecting`/`close` — pros valores em + português pedidos no critério de aceite). +- Tenants não têm campo `slug` no schema. Nome de instância (`tenant--`) gerado em + `src/modules/whatsapp/instance-name.util.ts` a partir de `nome_fantasia` (ou `razao_social` como + fallback), com uma tabela de substituição de acentos comuns em português (não + `normalize('NFD')`/regex de marcas diacríticas — evitado por instabilidade de encoding observada ao + gravar esse regex específico neste ambiente) + sufixo hex aleatório de 4 bytes. Unicidade é checada + antes do insert (até 5 tentativas) e reforçada pela constraint `@unique` do schema como último recurso. +- Ao criar uma instância, o tenant não escolhe qual `evolution_api_config` usar (a etapa não previu esse + fluxo) — `WhatsappService` seleciona automaticamente a config `ativo=true` mais antiga + (`orderBy: criado_em asc`) e grava o vínculo em `evolution_config_id`. Assume-se, na prática, uma única + config ativa por vez (mesmo espírito documentado no prompt para `master_email_config`: "geralmente só + um registro ativo, mas mantido como tabela"). Fica como ponto a revisitar se o produto precisar de + múltiplas Evolution APIs simultâneas roteadas por critério de negócio. +- `EmailService.resolverConfig` (usado por `AlertDispatchService.enviarEmail` e, portanto, pelos dois + endpoints de `testar-envio`) resolve o SMTP do tenant quando existe `tenant_email_config` com + `usar_config_padrao=false` (e todos os campos preenchidos — senão `BadRequestException` explícito); + caso contrário, usa o `master_email_config` `ativo=true` mais recente + (`orderBy: criado_em desc` — intencionalmente diferente do `asc` da Evolution config, para permitir + trocar o SMTP padrão da plataforma sem precisar excluir o registro antigo, só desativando o velho e + criando um novo). +- `PUT /tenant/email-config` com `usar_config_padrao=false` exige todos os campos de SMTP via + `@ValidateIf` declarativo no DTO (`UpdateTenantEmailConfigDto`); com `usar_config_padrao=true`, os + campos de SMTP são ignorados pelo service mesmo se enviados (não sobrescreve nem apaga o que já estava + salvo) — permite ao tenant alternar entre padrão/próprio sem perder a config própria já cadastrada. + `GET /tenant/email-config` some com os campos de SMTP próprio quando `usar_config_padrao=true` (só + devolve `{usar_config_padrao:true}`), como pedido no prompt ("ocultar/ignorar os campos"). +- Teste e2e novo, `test/integracoes.e2e-spec.ts` (11 testes), cobrindo todos os critérios de aceite: + CRUD de `evolution_api_config` com `api_key`/`smtp_senha` nunca em texto puro (conferido tanto na + resposta HTTP quanto decifrando o valor cru do banco via `EncryptionService` e comparando com o + original), `testar-conexao` contra o `FakeEvolutionServer`, criação de instância WhatsApp + QR code + + mudança de status para `conectado`, limite do plano bloqueando a instância excedente, teste de envio de + WhatsApp nos dois cenários (sucesso e falha simulada pela Evolution API, sempre HTTP 201 com + `{sucesso, erro?}` — nunca lança erro HTTP, como pedido no prompt), remoção de instância, isolamento + por RLS entre tenants (acessar instância de outro tenant retorna 404), CRUD de `master_email_config`, + tenant usando padrão vs. SMTP próprio (incluindo rejeição 400 sem os campos obrigatórios), teste de + e-mail nos dois modos (`nodemailer` mockado via `jest.mock('nodemailer')` — sem SMTP real disponível + neste ambiente — verificando que o `from` usado bate com a config resolvida em cada modo) e bloqueio de + todos os grupos de endpoint com o token errado (master vs. tenant). Rodado com `docker compose up -d + postgres redis` + `prisma migrate deploy` + `jest --config ./test/jest-e2e.json --runInBand` apontando + pro Postgres via `localhost` (porta mapeada do compose) — **54/54 testes e2e passando** (os 43 das + Etapas 1-3 + os 11 novos). `npm run lint`, `npx tsc --noEmit`, `npm run test` (unit, incluindo novo + `src/common/encryption.service.spec.ts`) e `npm run build` também passam sem erros. Build de produção + Docker (`docker build --target production`) verificado com sucesso, com smoke test manual confirmando + todas as rotas novas mapeadas no boot e `/health` (200) / endpoints protegidos sem token (401) contra a + imagem real. +- Duas dependências novas: `nodemailer`/`@types/nodemailer` (envio de e-mail) e `axios` (cliente HTTP + para a Evolution API) — adicionadas via `npm install`, sem `@nestjs/axios` (o projeto não usa nenhum + wrapper de HTTP client até agora; `axios` puro, encapsulado só dentro de `EvolutionApiClient`, seguiu o + mesmo espírito minimalista das dependências já escolhidas nas etapas anteriores). +- Nada relacionado ao código desta etapa, mas registrado por transparência: o ambiente local usado nesta + sessão tinha `node_modules/@types`, `dist/` (dono `root`, resíduo de builds Docker rodados como root em + sessão anterior) e um binário nativo de `argon2` compilado para `musl` em `node_modules/argon2/build` + (também resíduo de build Docker anterior, teve precedência indevida sobre o prebuild `glibc` correto + deste host) bloqueando `npm install`/`tsc`/os testes locais. Corrigido com `chown` (autorizado + interativamente pelo usuário) e removendo o `build/` nativo obsoleto — nenhuma mudança de código, só + limpeza de artefatos de build. Também diferente da Etapa 1: `docker build` funcionou nesta sessão com a + rede bridge padrão, sem precisar de `--network host`. + +--- + +## Etapa 5 — Painel Tenant: Clientes, Produtos e Serviços + +- [ ] CRUD de customers funcional, com soft delete e busca +- [ ] Criar customer além do limite do plano retorna erro claro +- [ ] CRUD de contatos aninhado funciona; contato principal único respeitado +- [ ] CRUD de produtos/serviços funcional, com soft delete e busca +- [ ] SKU duplicado no mesmo tenant retorna erro 409; permitido entre tenants +- [ ] Teste confirma que acessar recurso de outro tenant retorna 404 +- [ ] Regra de acesso master a dados de tenant definida e documentada + +**Observações:** + +--- + +## Etapa 6 — Vendas, Parcelas e Dashboards + +- [ ] Criar venda com itens e parcelas funciona em transação única +- [ ] Soma das parcelas validada contra o valor total da venda +- [ ] Cancelar venda cancela as parcelas pendentes associadas +- [ ] Venda confirmada não editável em itens/valores +- [ ] Marcar parcela como paga reflete no dashboard financeiro +- [ ] Dashboards semanal/mensal batem com dados de teste +- [ ] Endpoint de atualizar atrasadas funciona +- [ ] Isolamento por RLS reforçado e testado nesta etapa + +**Observações:** + +--- + +## Etapa 7 — Motor de Alertas (BullMQ) + +- [ ] Worker processa as duas filas independente do processo web +- [ ] Template do tenant sobrepõe padrão do master; fallback funciona +- [ ] Placeholders substituídos corretamente +- [ ] Job diário marca faturas/parcelas atrasadas corretamente +- [ ] Alerta de mensalidade chega no WhatsApp/e-mail do tenant (teste controlado) +- [ ] Alerta de cobrança chega no WhatsApp/e-mail do customer, com config correta do tenant +- [ ] Mesmo alerta não disparado duas vezes no mesmo dia +- [ ] Falha de envio reprocessada com retry; erro final registrado +- [ ] Endpoints de consulta de logs funcionam com filtros + +**Observações:** + +--- + +## Etapa 8 — Personalização Whitelabel e Ajustes Finais + +- [ ] Upload/remoção de logo e alteração de cor funcionam +- [ ] Endpoint público de tema retorna logo/cor sem autenticação +- [ ] CORS, Helmet e rate limiting revisados e ativos +- [ ] Nenhum segredo aparece em log ou resposta de API +- [ ] Swagger disponível cobrindo todos os módulos, com guards indicados +- [ ] Build de produção do Docker sem dependências de desenvolvimento +- [ ] Teste E2E do fluxo completo passa do início ao fim +- [ ] README final consolidado (dev, deploy, migrations, RLS) + +**Observações:** + +--- + +## Histórico de decisões e desvios do plano original + +Use esta seção para registrar qualquer decisão tomada durante a +implementação que se desvie do que foi descrito nos prompts originais +(ex: mudança de biblioteca, ajuste de schema, corte de escopo +combinado com o usuário). Formato sugerido: + +``` +[DATA] Etapa X — descrição da decisão/desvio e motivo +``` + +[2026-08-12] Etapa 1 — `@types/supertest` atualizado de `^6.0.2` para `^7.0.3` +para casar com a versão de `supertest`/`superagent` de fato instalada pelo npm; +sem isso `npm run lint` falhava com erros de tipo em `test/health.e2e-spec.ts`. +Nenhum comportamento de runtime muda, só a versão dos `@types`. + +[2026-08-13] Etapa 2 — Adicionada uma terceira connection string, +`DATABASE_URL_TENANT` (role `app_tenant`, sem `BYPASSRLS`), além da +`DATABASE_URL_MASTER` já prevista no prompt original. Motivo: a role de +`DATABASE_URL` (`seconectai`) é a superuser bootstrap do Postgres (criada +pelo `POSTGRES_USER` do docker-compose desde a Etapa 1) — e superusers +sempre ignoram Row Level Security, mesmo com as policies criadas na Etapa +1. Sem uma role de aplicação não-superuser dedicada (`app_tenant`), toda +query de `tenant_user` continuaria vendo dados de todos os tenants e o +isolamento por RLS seria só decorativo. As duas roles (`app_master` e +`app_tenant`) são criadas na migration manual +`prisma/migrations/20260813120000_auth_app_roles/`, com senhas +placeholder de dev documentadas em `.env.example` (mesmo padrão já usado +para `POSTGRES_PASSWORD`/`JWT_SECRET` — trocar em produção). + +[2026-08-13] Etapa 2 — `email` de `tenant_users` é único só por tenant, não +globalmente (`UNIQUE(tenant_id, email)` no schema original). Login e +forgot-password por e-mail, sem mais contexto (ex: subdomínio/slug do +tenant), não têm como resolver com segurança um e-mail que colida entre +dois tenants diferentes. Decisão: tratar 0 ou >1 `tenant_users` com o +mesmo e-mail como credencial inválida, em vez de arriscar autenticar/ +resetar a senha da conta errada. Fica como limitação conhecida — uma +etapa futura pode introduzir um identificador de tenant explícito no +login (subdomínio, slug) para eliminar essa ambiguidade. + +[2026-08-13] Etapa 3 — `TenantGuard` passou a checar o status do tenant a +cada request (consulta a `tenants` via `PrismaMasterService`), não só no +login. O prompt desta etapa já pedia isso explicitamente ("checar status +do tenant no login e no TenantGuard"), mas vale registrar: sem essa +checagem, suspender/cancelar um tenant não bloqueava de fato quem já +tinha um access token válido (até 15 min de janela, com a config +padrão). `changeStatus` também revoga os refresh tokens ativos dos +usuários do tenant ao suspender/cancelar, como defesa em profundidade +extra (não pedida explicitamente, mesmo espírito da revogação de família +de tokens da Etapa 2). + +[2026-08-13] Etapa 3 — `status` retirado do `UpdateTenantDto` (`PUT +/master/tenants/:id`), mesmo o prompt original listando `status` entre os +campos que esse endpoint atualiza. Toda transição de status passa +exclusivamente por `PATCH /master/tenants/:id/status`, o único caminho +que grava auditoria (`de`/`para`) e aplica bloqueio de acesso — permitir +mudar `status` também via PUT abriria um caminho para contornar essa +lógica. + +[2026-08-13] Etapa 3 — `package.json`: `test:e2e` passou a rodar com +`--runInBand`. A partir desta etapa os testes e2e leem estado global do +painel master (contagem de tenants por status, MRR, faturas do mês +corrente); com mais de um arquivo de teste, o Jest roda os arquivos em +paralelo por padrão, o que criaria uma fonte real de flakiness entre +`auth.e2e-spec.ts` e `master.e2e-spec.ts`. Rodar em série elimina isso. + +[2026-08-14] Etapa 4 — `AlertDispatchService` (e o `EmailService`/ +`EvolutionApiClient` por baixo dele) usam `PrismaMasterService` com filtro +manual de `tenant_id`, não `TenantContextService`/RLS — única exceção ao +padrão RLS-first do resto do projeto. Motivo: este service é o "serviço +central de envio, preparando a Etapa 7" e vai ser chamado pelo worker +BullMQ da Etapa 7 fora de qualquer request HTTP/contexto de tenant +autenticado, onde `TenantContextService.getTx()` não tem como funcionar +(não há `AsyncLocalStorage` ativo sem uma request em andamento). `tenant_id` +nunca vem de input do usuário final nesse service (sempre do JWT via +controller, e futuramente do job da fila), então o filtro manual continua +seguro. + +[2026-08-14] Etapa 4 — `AlertDispatchService.enviarWhatsapp` ganhou um +quarto parâmetro opcional, `instanceId?: string`, além dos três previstos +no prompt (`tenantId, numero, mensagem`). Sem ele, resolve a instância +`conectado` mais recente do tenant (uso futuro da Etapa 7); com ele, envia +por uma instância específica — necessário para +`POST /tenant/whatsapp/instancias/:id/testar-envio`, que testa uma +instância específica, não "uma instância qualquer do tenant", mantendo +ainda assim o pedido do prompt de reusar esse service nos endpoints de +teste de envio. + +[2026-08-14] Etapa 4 — `ENCRYPTION_KEY` (nova variável de ambiente +obrigatória) é reduzida a 32 bytes via SHA-256 antes de virar a chave +AES-256-GCM usada pelo `EncryptionService`, em vez de exigir um valor +hex/base64 de exatamente 32 bytes. Mesma lógica de validação de +`JWT_SECRET` (`@MinLength(32)`, qualquer string forte serve) — evita o +usuário ter que gerar/colar uma chave num formato binário específico. + +[2026-08-14] Etapa 4 — Não há instância real da Evolution API disponível +neste ambiente de desenvolvimento/CI. `EvolutionApiClient` foi implementado +contra a especificação pública documentada da API v2 e validado +ponta a ponta no teste e2e contra um servidor HTTP local que simula esse +contrato (`FakeEvolutionServer`, em `test/integracoes.e2e-spec.ts`), não +contra o serviço real. Confirmar contra uma instância real antes de operar +em produção. diff --git a/Etapas/seconectai-vendas-arquitetura.md b/Etapas/seconectai-vendas-arquitetura.md new file mode 100644 index 0000000..98b981e --- /dev/null +++ b/Etapas/seconectai-vendas-arquitetura.md @@ -0,0 +1,118 @@ +# SeConectai Vendas — Arquitetura + +CRM/controle de vendas multi-tenant, com um painel **master** (você, dono da +plataforma) e painéis de **tenant** (seus clientes, que por sua vez cadastram +os próprios clientes/vendas). + +## 1. Modelo de dados (resumo) + +``` +master_admins → você e sua equipe (login do painel master) +evolution_api_config → conexão(ões) com a Evolution API 2.3 +master_email_config → SMTP padrão da plataforma +planos → planos de assinatura que você vende +tenants → seus clientes (quem compra a plataforma) +tenant_faturas → mensalidades que o tenant te deve +tenant_users → usuários/login de cada tenant +tenant_whatsapp_instances → instância Evolution API de cada tenant +tenant_email_config → SMTP próprio do tenant (opcional) +alert_templates → mensagens (WhatsApp/e-mail) customizáveis +alert_logs → auditoria de todo alerta enviado +customers → clientes (empresas) de cada tenant +customer_contacts → contatos dentro dessas empresas +products_services → catálogo de produto/serviço por tenant +sales / sale_items / sale_payments → vendas, itens e parcelas a receber +audit_logs → trilha de auditoria geral +``` + +Arquivo completo: `seconectai-vendas-schema.sql`. + +## 2. Estratégia de multi-tenancy + +Banco único, todas as tabelas de dado operacional carregam `tenant_id`, com +**Row Level Security (RLS)** do Postgres isolando cada tenant. A aplicação +define `SET app.current_tenant_id = ''` na conexão/transação do +usuário logado; o Postgres bloqueia qualquer leitura/escrita fora do tenant +automaticamente — mesmo que haja um bug na query da aplicação. + +Por que essa abordagem em vez de "um schema por tenant" ou "um banco por +tenant": com um único Postgres em Docker, migrations, backup e escala +operacional ficam muito mais simples, e RLS já dá o isolamento de segurança +que schema-per-tenant tentaria resolver com mais complexidade operacional. +O usuário master (você) usa uma role de banco separada com `BYPASS ROW LEVEL +SECURITY`, para poder ver/gerenciar todos os tenants. + +## 3. Stack sugerida + +- **Backend:** Node.js + TypeScript, com NestJS (organização em módulos por + domínio: auth, tenants, customers, products, sales, alerts) ou Fastify se + quiser algo mais enxuto. Node se encaixa bem no seu workflow atual + (n8n, Evolution API, JS/TS já no dia a dia). +- **ORM:** Prisma — migrations versionadas, tipagem forte, e dá pra injetar + o `SET app.current_tenant_id` por transação com `$transaction`. +- **Banco:** PostgreSQL 16. +- **Fila/agendamento:** Redis + BullMQ, para dois tipos de job recorrente: + - varredura diária de vencimentos (`tenant_faturas` e `sale_payments`) + disparando alertas antes/no dia do vencimento; + - envio assíncrono de mensagens (não travar a request do usuário + esperando resposta da Evolution API/SMTP). +- **Autenticação:** JWT de curta duração (access token) + refresh token com + rotação, guardado com hash (nunca em texto puro) na tabela + `auth_refresh_tokens`. Senha com `argon2id` (preferível a bcrypt hoje). +- **Deploy:** Docker Compose com serviços `app`, `postgres`, `redis` — a + Evolution API pode rodar no mesmo compose ou em servidor separado (você + já mantém instância própria, então só precisa cadastrar a `base_url` + + `api_key` em `evolution_api_config`). + +## 4. Segurança — pontos que valem atenção na implementação + +- Hash de senha: `argon2id`, nunca MD5/SHA puro. +- Segredos (API key da Evolution, senha SMTP) criptografados em repouso + (AES-256-GCM) com chave fora do banco (variável de ambiente/secret + manager), não só "hash" — porque esses precisam ser recuperados em texto + puro para uso. +- Rate limiting no login (a tabela `login_attempts` já dá suporte a isso) e + bloqueio temporário após N tentativas falhas. +- 2FA (TOTP) pelo menos para `master_admins`, opcional para `tenant_admin`. +- RLS como camada de defesa adicional — mesmo com bug de aplicação, tenant A + não acessa dado de tenant B. +- HTTPS obrigatório, cookies de refresh token com `httpOnly` + `Secure` + + `SameSite=Strict`. +- Logs de auditoria (`audit_logs`, `alert_logs`) para rastrear quem fez o + quê — importante já que múltiplos tenants e usuários mexem no sistema. + +## 5. Fluxo de alertas (o motor comercial do sistema) + +Dois níveis, mesma mecânica: + +1. **Master → Tenant:** job diário verifica `tenant_faturas` com vencimento + próximo/vencido → gera mensagem a partir de `alert_templates` + (`tipo='mensalidade_tenant'`) → envia via WhatsApp (Evolution API, + instância própria do master) e/ou e-mail (`master_email_config`) → + grava em `alert_logs`. +2. **Tenant → Customer:** mesmo job (ou outro, escopado por tenant) verifica + `sale_payments` pendentes/atrasados de cada tenant → usa a instância + Evolution do próprio tenant (`tenant_whatsapp_instances`) e o SMTP dele + (ou o padrão da plataforma, se `usar_config_padrao=true`) → envia e + registra em `alert_logs`. + +Isso te dá, de graça, o mecanismo pra cobrar a própria mensalidade da +plataforma (item que você pediu explicitamente) usando a mesma +infraestrutura que o tenant usa pra cobrar os clientes dele. + +## 6. Próximos passos sugeridos + +Seguindo seu formato usual de build faseado (prompts sequenciais com +critério de aceite por etapa), a ordem natural seria: + +1. Setup do projeto (Docker Compose, Postgres, Prisma, estrutura NestJS) +2. Auth (master + tenant_user, JWT/refresh, RLS aplicado) +3. CRUD de tenants + planos + faturas (painel master) +4. Cadastro Evolution API + instâncias WhatsApp por tenant +5. CRUD de customers, products_services (painel tenant) +6. Vendas (sales/sale_items/sale_payments) + dashboards (views já prontas) +7. Motor de alertas (BullMQ + templates + logs) +8. Personalização visual (logo/cor por tenant) + +Posso detalhar qualquer uma dessas fases em um prompt de vibe coding +próprio, se quiser seguir esse fluxo. diff --git a/Etapas/seconectai-vendas-schema.sql b/Etapas/seconectai-vendas-schema.sql new file mode 100644 index 0000000..25ae684 --- /dev/null +++ b/Etapas/seconectai-vendas-schema.sql @@ -0,0 +1,451 @@ +-- ============================================================ +-- SeConectAI Vendas — Schema PostgreSQL (multi-tenant CRM/Vendas) +-- ============================================================ +-- Estratégia de multi-tenancy: banco único, isolado por tenant_id +-- + Row Level Security (RLS). Mais simples de operar em Docker/ +-- Docker Compose do que "schema por tenant" e escala bem até +-- centenas de tenants num único Postgres. +-- ============================================================ + +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid() + +-- ============================================================ +-- 1. NÍVEL MASTER (você — dono da plataforma) +-- ============================================================ + +CREATE TABLE master_admins ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + nome VARCHAR(150) NOT NULL, + email VARCHAR(150) UNIQUE NOT NULL, + senha_hash TEXT NOT NULL, -- argon2id ou bcrypt cost>=12 + ativo BOOLEAN NOT NULL DEFAULT TRUE, + mfa_secret TEXT, -- TOTP opcional, recomendado p/ master + ultimo_login TIMESTAMPTZ, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now(), + atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Conexão(ões) com a Evolution API 2.3, cadastrada pelo master. +-- Pode haver mais de um servidor Evolution (ex: para balancear carga). +CREATE TABLE evolution_api_config ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + nome VARCHAR(100) NOT NULL, -- "Servidor Principal" + base_url VARCHAR(255) NOT NULL, + api_key_cripto TEXT NOT NULL, -- criptografar em repouso (app-level, ex: AES-256-GCM) + versao VARCHAR(20) NOT NULL DEFAULT '2.3', + ativo BOOLEAN NOT NULL DEFAULT TRUE, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Config SMTP padrão da plataforma (fallback quando o tenant não +-- tem SMTP próprio cadastrado) +CREATE TABLE master_email_config ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + smtp_host VARCHAR(150) NOT NULL, + smtp_porta INTEGER NOT NULL, + smtp_usuario VARCHAR(150) NOT NULL, + smtp_senha_cripto TEXT NOT NULL, + email_remetente VARCHAR(150) NOT NULL, + ativo BOOLEAN NOT NULL DEFAULT TRUE, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE planos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + nome VARCHAR(100) NOT NULL, + descricao TEXT, + valor_mensal NUMERIC(10,2) NOT NULL, + limite_customers INTEGER, -- NULL = ilimitado + limite_usuarios INTEGER, + limite_instancias_whatsapp INTEGER NOT NULL DEFAULT 1, + ativo BOOLEAN NOT NULL DEFAULT TRUE, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- ============================================================ +-- 2. TENANTS — seus clientes que compram/usam a plataforma +-- ============================================================ + +CREATE TYPE tenant_status AS ENUM ('trial', 'ativo', 'inadimplente', 'suspenso', 'cancelado'); + +CREATE TABLE tenants ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + razao_social VARCHAR(200) NOT NULL, + nome_fantasia VARCHAR(200), + documento VARCHAR(20), -- CNPJ/CPF + email_contato VARCHAR(150) NOT NULL, + telefone VARCHAR(20), + logo_url TEXT, -- personalização visual (whitelabel) + cor_primaria VARCHAR(7), -- "#1a73e8" + plano_id UUID REFERENCES planos(id), + status tenant_status NOT NULL DEFAULT 'trial', + dia_vencimento_mensalidade SMALLINT NOT NULL DEFAULT 10 + CHECK (dia_vencimento_mensalidade BETWEEN 1 AND 28), + trial_expira_em TIMESTAMPTZ, + criado_por UUID REFERENCES master_admins(id), + criado_em TIMESTAMPTZ NOT NULL DEFAULT now(), + atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_tenants_status ON tenants(status); + +-- Faturas da mensalidade (você cobrando o tenant) +CREATE TYPE cobranca_status AS ENUM ('pendente', 'pago', 'atrasado', 'cancelado'); + +CREATE TABLE tenant_faturas ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + referencia VARCHAR(7) NOT NULL, -- '2026-08' + valor NUMERIC(10,2) NOT NULL, + vencimento DATE NOT NULL, + pago_em TIMESTAMPTZ, + status cobranca_status NOT NULL DEFAULT 'pendente', + alerta_enviado_em TIMESTAMPTZ, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE(tenant_id, referencia) +); + +CREATE INDEX idx_tenant_faturas_venc + ON tenant_faturas(vencimento) WHERE status IN ('pendente', 'atrasado'); + +-- ============================================================ +-- 3. USUÁRIOS E AUTENTICAÇÃO +-- ============================================================ + +CREATE TYPE user_role AS ENUM ('tenant_admin', 'vendedor', 'financeiro', 'visualizador'); + +CREATE TABLE tenant_users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + nome VARCHAR(150) NOT NULL, + email VARCHAR(150) NOT NULL, + senha_hash TEXT NOT NULL, + role user_role NOT NULL DEFAULT 'vendedor', + ativo BOOLEAN NOT NULL DEFAULT TRUE, + mfa_secret TEXT, + ultimo_login TIMESTAMPTZ, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE(tenant_id, email) +); + +CREATE INDEX idx_tenant_users_tenant ON tenant_users(tenant_id); + +-- Refresh tokens (JWT access token curto + refresh token com rotação) +CREATE TABLE auth_refresh_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + usuario_tipo VARCHAR(20) NOT NULL, -- 'master' | 'tenant_user' + usuario_id UUID NOT NULL, + token_hash TEXT NOT NULL, -- nunca guardar o token em texto puro + expira_em TIMESTAMPTZ NOT NULL, + revogado BOOLEAN NOT NULL DEFAULT FALSE, + ip_origem INET, + user_agent TEXT, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_refresh_tokens_usuario ON auth_refresh_tokens(usuario_tipo, usuario_id); + +CREATE TABLE password_reset_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + usuario_tipo VARCHAR(20) NOT NULL, + usuario_id UUID NOT NULL, + token_hash TEXT NOT NULL, + expira_em TIMESTAMPTZ NOT NULL, + usado BOOLEAN NOT NULL DEFAULT FALSE, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Registro de tentativas de login (para rate-limit/bloqueio de força bruta) +CREATE TABLE login_attempts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(150) NOT NULL, + usuario_tipo VARCHAR(20) NOT NULL, + sucesso BOOLEAN NOT NULL, + ip_origem INET, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_login_attempts_email_data ON login_attempts(email, criado_em); + +-- ============================================================ +-- 4. INTEGRAÇÕES POR TENANT (WhatsApp / E-mail / Templates) +-- ============================================================ + +CREATE TABLE tenant_whatsapp_instances ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + evolution_config_id UUID REFERENCES evolution_api_config(id), + instance_name VARCHAR(100) NOT NULL UNIQUE, -- nome único na Evolution API + numero_conectado VARCHAR(20), + status VARCHAR(30) NOT NULL DEFAULT 'desconectado', + -- desconectado | conectando | conectado | erro + qr_code_gerado_em TIMESTAMPTZ, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE tenant_email_config ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL UNIQUE REFERENCES tenants(id) ON DELETE CASCADE, + smtp_host VARCHAR(150), + smtp_porta INTEGER, + smtp_usuario VARCHAR(150), + smtp_senha_cripto TEXT, + email_remetente VARCHAR(150), + usar_config_padrao BOOLEAN NOT NULL DEFAULT TRUE, -- usa master_email_config se true + criado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Templates de mensagens (nível master = padrão global; nível tenant = customizado) +CREATE TABLE alert_templates ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, -- NULL = template padrão da plataforma + tipo VARCHAR(50) NOT NULL, + -- 'mensalidade_tenant' | 'cobranca_cliente' | 'venda_confirmada' | 'lembrete_vencimento' + canal VARCHAR(20) NOT NULL, -- 'whatsapp' | 'email' + assunto VARCHAR(200), -- só para email + corpo TEXT NOT NULL, -- placeholders: {{nome}}, {{valor}}, {{vencimento}} + ativo BOOLEAN NOT NULL DEFAULT TRUE, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Log/auditoria de todo alerta disparado (master->tenant e tenant->customer) +CREATE TABLE alert_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, + destinatario_tipo VARCHAR(20) NOT NULL, -- 'tenant' | 'customer' + destinatario_id UUID NOT NULL, -- id do tenant ou do customer + canal VARCHAR(20) NOT NULL, -- whatsapp | email + template_id UUID REFERENCES alert_templates(id), + conteudo_enviado TEXT, + status VARCHAR(20) NOT NULL DEFAULT 'enviado', -- enviado | falhou | entregue | lido + erro TEXT, + enviado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_alert_logs_tenant ON alert_logs(tenant_id, enviado_em); + +-- ============================================================ +-- 5. CLIENTES DO TENANT ("customers" — empresas atendidas) +-- ============================================================ + +CREATE TABLE customers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + razao_social VARCHAR(200) NOT NULL, + nome_fantasia VARCHAR(200), + documento VARCHAR(20), + email VARCHAR(150), + telefone_whatsapp VARCHAR(20), + endereco TEXT, + cidade VARCHAR(100), + uf CHAR(2), + observacoes TEXT, + ativo BOOLEAN NOT NULL DEFAULT TRUE, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now(), + atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_customers_tenant ON customers(tenant_id); + +CREATE TABLE customer_contacts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, + nome VARCHAR(150) NOT NULL, + cargo VARCHAR(100), + email VARCHAR(150), + telefone_whatsapp VARCHAR(20), + principal BOOLEAN NOT NULL DEFAULT FALSE, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- ============================================================ +-- 6. PRODUTOS E SERVIÇOS DO TENANT +-- ============================================================ + +CREATE TYPE item_tipo AS ENUM ('produto', 'servico'); + +CREATE TABLE products_services ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + tipo item_tipo NOT NULL, + nome VARCHAR(200) NOT NULL, + descricao TEXT, + sku VARCHAR(50), + valor_venda NUMERIC(12,2) NOT NULL, + valor_custo NUMERIC(12,2), + unidade VARCHAR(20) NOT NULL DEFAULT 'un', -- un, hora, mês... + recorrente BOOLEAN NOT NULL DEFAULT FALSE, -- ex: serviço de mensalidade + ativo BOOLEAN NOT NULL DEFAULT TRUE, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now(), + atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE(tenant_id, sku) +); + +CREATE INDEX idx_products_tenant ON products_services(tenant_id); + +-- ============================================================ +-- 7. VENDAS +-- ============================================================ + +CREATE TYPE venda_status AS ENUM ('orcamento', 'confirmada', 'cancelada', 'concluida'); + +CREATE TABLE sales ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + customer_id UUID NOT NULL REFERENCES customers(id), + vendedor_id UUID REFERENCES tenant_users(id), + numero_venda SERIAL, + status venda_status NOT NULL DEFAULT 'confirmada', + valor_total NUMERIC(12,2) NOT NULL DEFAULT 0, + desconto NUMERIC(12,2) NOT NULL DEFAULT 0, + observacoes TEXT, + data_venda DATE NOT NULL DEFAULT CURRENT_DATE, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now(), + atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_sales_tenant_data ON sales(tenant_id, data_venda); +CREATE INDEX idx_sales_customer ON sales(customer_id); + +CREATE TABLE sale_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + sale_id UUID NOT NULL REFERENCES sales(id) ON DELETE CASCADE, + product_service_id UUID NOT NULL REFERENCES products_services(id), + quantidade NUMERIC(10,2) NOT NULL DEFAULT 1, + valor_unitario NUMERIC(12,2) NOT NULL, + valor_total NUMERIC(12,2) NOT NULL +); + +CREATE INDEX idx_sale_items_sale ON sale_items(sale_id); + +-- Parcelas/recebimentos: contas a receber do tenant junto ao customer +CREATE TYPE parcela_status AS ENUM ('pendente', 'pago', 'atrasado', 'cancelado'); + +CREATE TABLE sale_payments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + sale_id UUID NOT NULL REFERENCES sales(id) ON DELETE CASCADE, + numero_parcela SMALLINT NOT NULL DEFAULT 1, + valor NUMERIC(12,2) NOT NULL, + vencimento DATE NOT NULL, + pago_em TIMESTAMPTZ, + forma_pagamento VARCHAR(30), -- pix, boleto, cartao, dinheiro + status parcela_status NOT NULL DEFAULT 'pendente', + alerta_enviado_em TIMESTAMPTZ, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_sale_payments_venc + ON sale_payments(vencimento) WHERE status IN ('pendente', 'atrasado'); + +-- ============================================================ +-- 8. AUDITORIA GERAL +-- ============================================================ + +CREATE TABLE audit_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID REFERENCES tenants(id) ON DELETE SET NULL, + usuario_tipo VARCHAR(20) NOT NULL, -- master | tenant_user + usuario_id UUID, + acao VARCHAR(100) NOT NULL, -- login, criar_venda, excluir_cliente... + entidade VARCHAR(50), + entidade_id UUID, + detalhes JSONB, + ip_origem INET, + criado_em TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_audit_tenant_data ON audit_logs(tenant_id, criado_em); + +-- ============================================================ +-- 9. VIEWS DE DASHBOARD (usadas no painel do tenant) +-- ============================================================ + +CREATE VIEW vw_resumo_vendas_semanal AS +SELECT + s.tenant_id, + date_trunc('week', s.data_venda)::date AS semana, + COUNT(*) AS qtd_vendas, + SUM(s.valor_total) AS total_vendido +FROM sales s +WHERE s.status IN ('confirmada', 'concluida') +GROUP BY s.tenant_id, date_trunc('week', s.data_venda); + +CREATE VIEW vw_resumo_vendas_mensal AS +SELECT + s.tenant_id, + date_trunc('month', s.data_venda)::date AS mes, + COUNT(*) AS qtd_vendas, + SUM(s.valor_total) AS total_vendido +FROM sales s +WHERE s.status IN ('confirmada', 'concluida') +GROUP BY s.tenant_id, date_trunc('month', s.data_venda); + +CREATE VIEW vw_recebimentos AS +SELECT + sp.id, + s.tenant_id, + s.customer_id, + sp.valor, + sp.vencimento, + sp.status, + sp.pago_em +FROM sale_payments sp +JOIN sales s ON s.id = sp.sale_id; + +-- ============================================================ +-- 10. ROW LEVEL SECURITY — isolamento entre tenants +-- ============================================================ +-- A aplicação deve definir, por conexão/transação: +-- SET app.current_tenant_id = ''; +-- Usuários master (que enxergam todos os tenants) usam uma role +-- de banco separada (ex: app_master) que faz BYPASS RLS, ou uma +-- policy adicional que libera quando app.is_master = 'true'. + +ALTER TABLE customers ENABLE ROW LEVEL SECURITY; +ALTER TABLE customer_contacts ENABLE ROW LEVEL SECURITY; +ALTER TABLE products_services ENABLE ROW LEVEL SECURITY; +ALTER TABLE sales ENABLE ROW LEVEL SECURITY; +ALTER TABLE sale_items ENABLE ROW LEVEL SECURITY; +ALTER TABLE sale_payments ENABLE ROW LEVEL SECURITY; +ALTER TABLE tenant_users ENABLE ROW LEVEL SECURITY; +ALTER TABLE tenant_whatsapp_instances ENABLE ROW LEVEL SECURITY; +ALTER TABLE tenant_email_config ENABLE ROW LEVEL SECURITY; + +CREATE POLICY tenant_isolation_customers ON customers + USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); + +CREATE POLICY tenant_isolation_products ON products_services + USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); + +CREATE POLICY tenant_isolation_sales ON sales + USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); + +CREATE POLICY tenant_isolation_tenant_users ON tenant_users + USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); + +CREATE POLICY tenant_isolation_whatsapp ON tenant_whatsapp_instances + USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); + +CREATE POLICY tenant_isolation_email_config ON tenant_email_config + USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); + +-- sale_items e customer_contacts não têm tenant_id direto: isolar via join +CREATE POLICY tenant_isolation_sale_items ON sale_items + USING (sale_id IN ( + SELECT id FROM sales + WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid + )); + +CREATE POLICY tenant_isolation_sale_payments ON sale_payments + USING (sale_id IN ( + SELECT id FROM sales + WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid + )); + +CREATE POLICY tenant_isolation_customer_contacts ON customer_contacts + USING (customer_id IN ( + SELECT id FROM customers + WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid + )); + +-- Fim do schema diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec5a2a2 --- /dev/null +++ b/README.md @@ -0,0 +1,172 @@ +# SeConectai Vendas + +CRM/controle de vendas multi-tenant. Um painel **master** (dono da +plataforma) administra **tenants** (clientes), que por sua vez cadastram +seus próprios clientes ("customers"), produtos/serviços e vendas. + +Stack: Node.js 20 + TypeScript, NestJS, PostgreSQL 16 + Prisma, Redis, +Docker/Docker Compose. Multi-tenancy via banco único, isolado por +`tenant_id` + Row Level Security (RLS) do Postgres. + +## Subindo o ambiente do zero + +1. Copie o arquivo de variáveis de ambiente e ajuste se necessário: + + ```bash + cp .env.example .env + ``` + +2. Suba os containers (Postgres, Redis e a API com hot-reload em modo dev): + + ```bash + docker compose up + ``` + + Isso builda a imagem `app` (stage `dev`), sobe `postgres` e `redis` com + healthcheck, e só inicia a API depois que as duas dependências estiverem + saudáveis. `docker-compose.override.yml` é aplicado automaticamente e monta + o código-fonte local no container, então alterações em `src/` reiniciam a + API sozinhas (`npm run dev` / `nest start --watch`). + +3. Rode as migrations do Prisma (tabelas, enums, índices) e a migration + manual de RLS/views/checks: + + ```bash + docker compose exec app npx prisma migrate deploy + ``` + + Em desenvolvimento, para criar uma nova migration a partir de mudanças no + `schema.prisma`: + + ```bash + docker compose exec app npx prisma migrate dev + ``` + +4. Confirme que a API está de pé: + + ```bash + curl http://localhost:3000/health + ``` + + Deve retornar `200` com `{"status":"ok","database":"up",...}`. + +## Rodando sem Docker (opcional) + +Requer Node.js 20+, um Postgres 16 e um Redis acessíveis localmente. + +```bash +npm install +npm run prisma:generate +npm run prisma:migrate +npm run dev +``` + +## Variáveis de ambiente + +Veja [.env.example](.env.example) — todas as chaves usadas pela aplicação e +pelo `docker-compose.yml` (credenciais do Postgres, `DATABASE_URL` e +variantes, Redis, `JWT_SECRET` e demais parâmetros de autenticação) estão +documentadas lá. + +## Autenticação + +`src/modules/auth/` implementa login/refresh/logout/reset de senha para os +dois tipos de usuário: + +- `POST /auth/master/login` e `POST /auth/tenant/login` — e-mail+senha → + access token (JWT, 15 min por padrão) + refresh token opaco (rotacionado + a cada uso, guardado só como hash sha256 em `auth_refresh_tokens`). +- `POST /auth/refresh` — rotaciona o refresh token; reuso de um token já + rotacionado é tratado como indício de roubo e revoga toda a família de + tokens do usuário. +- `POST /auth/logout` — revoga o refresh token informado. +- `POST /auth/password/forgot` / `POST /auth/password/reset` — gera token + de reset (hash, expiração curta, uso único); por ora o token é só logado + no console (envio de e-mail real é da Etapa 4). +- Senhas com `argon2id` (parâmetros OWASP 2024). Bloqueio de força bruta + por e-mail após `LOGIN_MAX_ATTEMPTS` falhas em `LOGIN_LOCKOUT_WINDOW_MINUTES` + (tabela `login_attempts`), mais um rate limit por IP nas rotas de login + (`@nestjs/throttler`). +- `JwtAuthGuard`, `MasterGuard`, `TenantGuard`, `RolesGuard` (+ + `@Roles(...)`) e o decorator `@CurrentUser()` — ver + `GET /auth/me`, `GET /auth/master/admins` e `GET /auth/tenant/users` em + `auth.controller.ts` para exemplos de uso de cada guard. + +## Migrations e RLS + +O schema em `prisma/schema.prisma` é a tradução 1:1 de +`Etapas/seconectai-vendas-schema.sql` (fonte de verdade). Construtos que o +Prisma DSL não expressa nativamente — views de dashboard, `CHECK` +constraints e as `CREATE POLICY` de Row Level Security — vivem numa +migration manual em `prisma/migrations/_rls_views_constraints/`, +aplicada junto com as migrations geradas automaticamente pelo Prisma +(`prisma migrate deploy` roda todas em ordem). + +Uma segunda migration manual +(`prisma/migrations/20260813120000_auth_app_roles/`) cria duas roles de +banco usadas em runtime: + +- **`app_master`** (`LOGIN` + `BYPASSRLS`, conectada via + `DATABASE_URL_MASTER`) — usada pelo painel master, que precisa enxergar + dados de todos os tenants. +- **`app_tenant`** (`LOGIN` comum, sem `BYPASSRLS`, conectada via + `DATABASE_URL_TENANT`) — usada por toda query autenticada como + `tenant_user`. + +Isso é necessário porque a role de `DATABASE_URL` (`seconectai`, dona das +tabelas) é a superuser bootstrap do Postgres criada pelo `POSTGRES_USER` +do docker-compose — e superusers **sempre** ignoram Row Level Security, +mesmo com as policies criadas. Sem uma role não-superuser dedicada, o +isolamento por tenant seria só decorativo. + +Por request, o `TenantRlsInterceptor` (registrado globalmente) identifica +requests autenticadas como `tenant_user` e, via `TenantContextService`, +abre uma transação na conexão `app_tenant` executando: + +```sql +SELECT set_config('app.current_tenant_id', '', true); -- equivalente a SET LOCAL +``` + +sempre usando o `tenant_id` do JWT da sessão — nunca um valor vindo de +input do usuário (body/query). Serviços que precisam rodar dentro desse +contexto usam `TenantContextService.getTx()` (ver +`AuthController.listTenantUsers` como exemplo). Requests master usam +`PrismaMasterService` (conexão `app_master`, sem passar pelas policies). + +## Scripts + +| Script | Descrição | +|----------------------------|-----------------------------------------------------| +| `npm run dev` | Inicia a API em modo watch | +| `npm run build` | Compila TypeScript para `dist/` | +| `npm run start:prod` | Roda a build de produção (`dist/main.js`) | +| `npm run lint` | ESLint (com `--fix`) | +| `npm run test` | Testes unitários (Jest) | +| `npm run test:e2e` | Testes end-to-end (Jest + Supertest) | +| `npm run prisma:generate` | Gera o Prisma Client | +| `npm run prisma:migrate` | Cria/aplica migration em desenvolvimento | +| `npm run prisma:migrate:deploy` | Aplica migrations pendentes (produção/CI) | +| `npm run prisma:studio` | Abre o Prisma Studio | + +## Estrutura do projeto + +``` +src/ +├── main.ts +├── app.module.ts +├── config/ # ConfigModule, validação de env, PrismaService/PrismaMasterService/ +│ # PrismaTenantService, TenantContextService (mecanismo de RLS por request) +└── modules/ + ├── health/ # GET /health + └── auth/ # login/refresh/logout/reset, guards, decorators, JwtStrategy +prisma/ +├── schema.prisma +└── migrations/ +Etapas/ # Prompts e controle de progresso do build faseado +``` + +## Progresso do build + +Este projeto é construído em etapas. O estado oficial de cada etapa (o que +já foi concluído, o que falta, decisões tomadas) fica em +[`Etapas/09-controle-de-progresso.md`](Etapas/09-controle-de-progresso.md). diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..7e91d9d --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,12 @@ +services: + app: + build: + target: dev + command: npm run dev + environment: + NODE_ENV: development + volumes: + - .:/app + - /app/node_modules + healthcheck: + disable: true diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b15e349 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,55 @@ +services: + app: + build: + context: . + target: production + restart: unless-stopped + ports: + - '${PORT:-3000}:3000' + env_file: + - .env + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ['CMD', 'node', '-e', "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-seconectai} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-seconectai} + POSTGRES_DB: ${POSTGRES_DB:-seconectai_vendas} + ports: + - '${POSTGRES_PORT:-5432}:5432' + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-seconectai} -d ${POSTGRES_DB:-seconectai_vendas}'] + interval: 5s + timeout: 5s + retries: 10 + + redis: + image: redis:7-alpine + restart: unless-stopped + ports: + - '${REDIS_PORT:-6379}:6379' + volumes: + - redis_data:/data + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 5s + timeout: 5s + retries: 10 + +volumes: + postgres_data: + redis_data: diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..9c0acc3 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,34 @@ +// @ts-check +import eslint from '@eslint/js'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['eslint.config.mjs', 'dist/**', 'node_modules/**', 'coverage/**'], + }, + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + eslintPluginPrettierRecommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + sourceType: 'commonjs', + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn', + }, + }, +); diff --git a/nest-cli.json b/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..82cc77b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,10319 @@ +{ + "name": "seconectai-vendas", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "seconectai-vendas", + "version": "0.1.0", + "license": "UNLICENSED", + "dependencies": { + "@nestjs/common": "^10.4.15", + "@nestjs/config": "^3.3.0", + "@nestjs/core": "^10.4.15", + "@nestjs/jwt": "^11.0.2", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^10.4.15", + "@nestjs/throttler": "^6.5.0", + "@prisma/client": "^5.22.0", + "argon2": "^0.45.1", + "axios": "^1.19.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "nodemailer": "^9.0.5", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@nestjs/cli": "^10.4.9", + "@nestjs/schematics": "^10.2.3", + "@nestjs/testing": "^10.4.15", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^20.17.10", + "@types/nodemailer": "^8.0.1", + "@types/passport-jwt": "^4.0.1", + "@types/supertest": "^7.0.3", + "eslint": "^9.17.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "globals": "^15.14.0", + "jest": "^29.7.0", + "prettier": "^3.4.2", + "prisma": "^5.22.0", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.1" + } + }, + "node_modules/@angular-devkit/core": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", + "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", + "integrity": "sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.3.11.tgz", + "integrity": "sha512-kcOMqp+PHAKkqRad7Zd7PbpqJ0LqLaNZdY1+k66lLWmkEBozgq8v4ASn/puPWf9Bo0HpCiK+EzLf0VHE8Z/y6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "ansi-colors": "4.1.3", + "inquirer": "9.2.15", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/inquirer": { + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", + "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ljharb/through": "^2.3.12", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^3.2.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@ljharb/through": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.14.tgz", + "integrity": "sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/cli": { + "version": "10.4.9", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", + "integrity": "sha512-s8qYd97bggqeK7Op3iD49X2MpFtW4LVNLAwXFkfbRxKME6IYT7X0muNTJ2+QfI8hpbNx9isWkrLWIp+g5FOhiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "@angular-devkit/schematics-cli": "17.3.11", + "@nestjs/schematics": "^10.0.1", + "chalk": "4.1.2", + "chokidar": "3.6.0", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.0.2", + "glob": "10.4.5", + "inquirer": "8.2.6", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tree-kill": "1.2.2", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.7.2", + "webpack": "5.97.1", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 16.14" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/cli/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nestjs/cli/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nestjs/cli/node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@nestjs/cli/node_modules/webpack": { + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.22.tgz", + "integrity": "sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw==", + "license": "MIT", + "dependencies": { + "file-type": "20.4.1", + "iterare": "1.2.1", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.3.0.tgz", + "integrity": "sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.5", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/core": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.22.tgz", + "integrity": "sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/jwt": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.2.tgz", + "integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.10", + "jsonwebtoken": "9.0.3" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/passport": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", + "integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "passport": "^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, + "node_modules/@nestjs/platform-express": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.22.tgz", + "integrity": "sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==", + "license": "MIT", + "dependencies": { + "body-parser": "1.20.4", + "cors": "2.8.5", + "express": "4.22.1", + "multer": "2.0.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz", + "integrity": "sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "comment-json": "4.2.5", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/schematics/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/testing": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.22.tgz", + "integrity": "sha512-HO9aPus3bAedAC+jKVAA8jTdaj4fs5M9fing4giHrcYV2txe9CvC1l1WAjwQ9RDhEHdugjY4y+FZA/U/YqPZrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@nestjs/throttler": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.5.0.tgz", + "integrity": "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/inflate/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@tokenizer/inflate/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.1.tgz", + "integrity": "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.11", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.11.tgz", + "integrity": "sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.1.tgz", + "integrity": "sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", + "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/type-utils": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.67.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz", + "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz", + "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.67.0", + "@typescript-eslint/types": "^8.67.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/project-service/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz", + "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz", + "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz", + "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/types": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz", + "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz", + "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.67.0", + "@typescript-eslint/tsconfig-utils": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz", + "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz", + "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argon2": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.45.1.tgz", + "integrity": "sha512-skm+/WCjkGqCQxF7FG1LuZXM5yvbFjgbfiCGsud2oLgaDhh6b6dbH0b1EkghbM+xx4Bj8Ape+KKgixoIlWZicQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@phc/format": "^1.0.0", + "cross-env": "^10.1.0", + "node-addon-api": "^8.9.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz", + "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz", + "integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.405", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.405.tgz", + "integrity": "sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.13" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", + "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.10.tgz", + "integrity": "sha512-xJxrdqvbl2rtn2MaUJrUejz8J7/uZNC0V77oks2LxYrO/+ZtVpRmz+fEQMuu6VusnEB1fmpByiLS1WXecOnAnw==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "8.9.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz", + "integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nodemailer": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.5.tgz", + "integrity": "sha512-wvjiKvjczmsN7U/8006JOdXubgBk2XFAbioDMbT+sM7cPs0QrhJTa6KBRX7P5REGGkDcLUz/EarWidb8G8C1jQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.50.0.tgz", + "integrity": "sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.12", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.12.tgz", + "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.8.5", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-loader": { + "version": "9.6.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.6.2.tgz", + "integrity": "sha512-R4iuczmtgxvtuI556s+hTZ6/7Ee03VCAk/l/M8LY1OAsUgB7YydsCxkgq9D9pKRaD7GJqUi2u8fp9zZP/ufjKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "picomatch": "^4.0.0", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "loader-utils": "*", + "typescript": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "loader-utils": { + "optional": true + } + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz", + "integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.67.0", + "@typescript-eslint/parser": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.109.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.109.2.tgz", + "integrity": "sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.24.4", + "es-module-lexer": "^2.1.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.6.1", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..288b220 --- /dev/null +++ b/package.json @@ -0,0 +1,87 @@ +{ + "name": "seconectai-vendas", + "version": "0.1.0", + "description": "SeConectai Vendas — CRM/controle de vendas multi-tenant", + "license": "UNLICENSED", + "private": true, + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "dev": "nest start --watch", + "start": "nest start", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:e2e": "jest --config ./test/jest-e2e.json --runInBand", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:migrate:deploy": "prisma migrate deploy", + "prisma:studio": "prisma studio" + }, + "dependencies": { + "@nestjs/common": "^10.4.15", + "@nestjs/config": "^3.3.0", + "@nestjs/core": "^10.4.15", + "@nestjs/jwt": "^11.0.2", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^10.4.15", + "@nestjs/throttler": "^6.5.0", + "@prisma/client": "^5.22.0", + "argon2": "^0.45.1", + "axios": "^1.19.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "nodemailer": "^9.0.5", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@nestjs/cli": "^10.4.9", + "@nestjs/schematics": "^10.2.3", + "@nestjs/testing": "^10.4.15", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^20.17.10", + "@types/nodemailer": "^8.0.1", + "@types/passport-jwt": "^4.0.1", + "@types/supertest": "^7.0.3", + "eslint": "^9.17.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "globals": "^15.14.0", + "jest": "^29.7.0", + "prettier": "^3.4.2", + "prisma": "^5.22.0", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.1" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/prisma/migrations/20260812160945_init/migration.sql b/prisma/migrations/20260812160945_init/migration.sql new file mode 100644 index 0000000..7b04c8f --- /dev/null +++ b/prisma/migrations/20260812160945_init/migration.sql @@ -0,0 +1,451 @@ +-- CreateEnum +CREATE TYPE "tenant_status" AS ENUM ('trial', 'ativo', 'inadimplente', 'suspenso', 'cancelado'); + +-- CreateEnum +CREATE TYPE "cobranca_status" AS ENUM ('pendente', 'pago', 'atrasado', 'cancelado'); + +-- CreateEnum +CREATE TYPE "user_role" AS ENUM ('tenant_admin', 'vendedor', 'financeiro', 'visualizador'); + +-- CreateEnum +CREATE TYPE "item_tipo" AS ENUM ('produto', 'servico'); + +-- CreateEnum +CREATE TYPE "venda_status" AS ENUM ('orcamento', 'confirmada', 'cancelada', 'concluida'); + +-- CreateEnum +CREATE TYPE "parcela_status" AS ENUM ('pendente', 'pago', 'atrasado', 'cancelado'); + +-- CreateTable +CREATE TABLE "master_admins" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "nome" VARCHAR(150) NOT NULL, + "email" VARCHAR(150) NOT NULL, + "senha_hash" TEXT NOT NULL, + "ativo" BOOLEAN NOT NULL DEFAULT true, + "mfa_secret" TEXT, + "ultimo_login" TIMESTAMPTZ(6), + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "atualizado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "master_admins_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "evolution_api_config" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "nome" VARCHAR(100) NOT NULL, + "base_url" VARCHAR(255) NOT NULL, + "api_key_cripto" TEXT NOT NULL, + "versao" VARCHAR(20) NOT NULL DEFAULT '2.3', + "ativo" BOOLEAN NOT NULL DEFAULT true, + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "evolution_api_config_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "master_email_config" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "smtp_host" VARCHAR(150) NOT NULL, + "smtp_porta" INTEGER NOT NULL, + "smtp_usuario" VARCHAR(150) NOT NULL, + "smtp_senha_cripto" TEXT NOT NULL, + "email_remetente" VARCHAR(150) NOT NULL, + "ativo" BOOLEAN NOT NULL DEFAULT true, + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "master_email_config_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "planos" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "nome" VARCHAR(100) NOT NULL, + "descricao" TEXT, + "valor_mensal" DECIMAL(10,2) NOT NULL, + "limite_customers" INTEGER, + "limite_usuarios" INTEGER, + "limite_instancias_whatsapp" INTEGER NOT NULL DEFAULT 1, + "ativo" BOOLEAN NOT NULL DEFAULT true, + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "planos_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "tenants" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "razao_social" VARCHAR(200) NOT NULL, + "nome_fantasia" VARCHAR(200), + "documento" VARCHAR(20), + "email_contato" VARCHAR(150) NOT NULL, + "telefone" VARCHAR(20), + "logo_url" TEXT, + "cor_primaria" VARCHAR(7), + "plano_id" UUID, + "status" "tenant_status" NOT NULL DEFAULT 'trial', + "dia_vencimento_mensalidade" SMALLINT NOT NULL DEFAULT 10, + "trial_expira_em" TIMESTAMPTZ(6), + "criado_por" UUID, + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "atualizado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "tenants_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "tenant_faturas" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "tenant_id" UUID NOT NULL, + "referencia" VARCHAR(7) NOT NULL, + "valor" DECIMAL(10,2) NOT NULL, + "vencimento" DATE NOT NULL, + "pago_em" TIMESTAMPTZ(6), + "status" "cobranca_status" NOT NULL DEFAULT 'pendente', + "alerta_enviado_em" TIMESTAMPTZ(6), + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "tenant_faturas_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "tenant_users" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "tenant_id" UUID NOT NULL, + "nome" VARCHAR(150) NOT NULL, + "email" VARCHAR(150) NOT NULL, + "senha_hash" TEXT NOT NULL, + "role" "user_role" NOT NULL DEFAULT 'vendedor', + "ativo" BOOLEAN NOT NULL DEFAULT true, + "mfa_secret" TEXT, + "ultimo_login" TIMESTAMPTZ(6), + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "tenant_users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_refresh_tokens" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "usuario_tipo" VARCHAR(20) NOT NULL, + "usuario_id" UUID NOT NULL, + "token_hash" TEXT NOT NULL, + "expira_em" TIMESTAMPTZ(6) NOT NULL, + "revogado" BOOLEAN NOT NULL DEFAULT false, + "ip_origem" INET, + "user_agent" TEXT, + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "auth_refresh_tokens_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "password_reset_tokens" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "usuario_tipo" VARCHAR(20) NOT NULL, + "usuario_id" UUID NOT NULL, + "token_hash" TEXT NOT NULL, + "expira_em" TIMESTAMPTZ(6) NOT NULL, + "usado" BOOLEAN NOT NULL DEFAULT false, + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "password_reset_tokens_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "login_attempts" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "email" VARCHAR(150) NOT NULL, + "usuario_tipo" VARCHAR(20) NOT NULL, + "sucesso" BOOLEAN NOT NULL, + "ip_origem" INET, + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "login_attempts_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "tenant_whatsapp_instances" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "tenant_id" UUID NOT NULL, + "evolution_config_id" UUID, + "instance_name" VARCHAR(100) NOT NULL, + "numero_conectado" VARCHAR(20), + "status" VARCHAR(30) NOT NULL DEFAULT 'desconectado', + "qr_code_gerado_em" TIMESTAMPTZ(6), + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "tenant_whatsapp_instances_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "tenant_email_config" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "tenant_id" UUID NOT NULL, + "smtp_host" VARCHAR(150), + "smtp_porta" INTEGER, + "smtp_usuario" VARCHAR(150), + "smtp_senha_cripto" TEXT, + "email_remetente" VARCHAR(150), + "usar_config_padrao" BOOLEAN NOT NULL DEFAULT true, + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "tenant_email_config_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "alert_templates" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "tenant_id" UUID, + "tipo" VARCHAR(50) NOT NULL, + "canal" VARCHAR(20) NOT NULL, + "assunto" VARCHAR(200), + "corpo" TEXT NOT NULL, + "ativo" BOOLEAN NOT NULL DEFAULT true, + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "alert_templates_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "alert_logs" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "tenant_id" UUID, + "destinatario_tipo" VARCHAR(20) NOT NULL, + "destinatario_id" UUID NOT NULL, + "canal" VARCHAR(20) NOT NULL, + "template_id" UUID, + "conteudo_enviado" TEXT, + "status" VARCHAR(20) NOT NULL DEFAULT 'enviado', + "erro" TEXT, + "enviado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "alert_logs_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "customers" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "tenant_id" UUID NOT NULL, + "razao_social" VARCHAR(200) NOT NULL, + "nome_fantasia" VARCHAR(200), + "documento" VARCHAR(20), + "email" VARCHAR(150), + "telefone_whatsapp" VARCHAR(20), + "endereco" TEXT, + "cidade" VARCHAR(100), + "uf" CHAR(2), + "observacoes" TEXT, + "ativo" BOOLEAN NOT NULL DEFAULT true, + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "atualizado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "customers_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "customer_contacts" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "customer_id" UUID NOT NULL, + "nome" VARCHAR(150) NOT NULL, + "cargo" VARCHAR(100), + "email" VARCHAR(150), + "telefone_whatsapp" VARCHAR(20), + "principal" BOOLEAN NOT NULL DEFAULT false, + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "customer_contacts_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "products_services" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "tenant_id" UUID NOT NULL, + "tipo" "item_tipo" NOT NULL, + "nome" VARCHAR(200) NOT NULL, + "descricao" TEXT, + "sku" VARCHAR(50), + "valor_venda" DECIMAL(12,2) NOT NULL, + "valor_custo" DECIMAL(12,2), + "unidade" VARCHAR(20) NOT NULL DEFAULT 'un', + "recorrente" BOOLEAN NOT NULL DEFAULT false, + "ativo" BOOLEAN NOT NULL DEFAULT true, + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "atualizado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "products_services_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "sales" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "tenant_id" UUID NOT NULL, + "customer_id" UUID NOT NULL, + "vendedor_id" UUID, + "numero_venda" SERIAL NOT NULL, + "status" "venda_status" NOT NULL DEFAULT 'confirmada', + "valor_total" DECIMAL(12,2) NOT NULL DEFAULT 0, + "desconto" DECIMAL(12,2) NOT NULL DEFAULT 0, + "observacoes" TEXT, + "data_venda" DATE NOT NULL DEFAULT CURRENT_TIMESTAMP, + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "atualizado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "sales_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "sale_items" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "sale_id" UUID NOT NULL, + "product_service_id" UUID NOT NULL, + "quantidade" DECIMAL(10,2) NOT NULL DEFAULT 1, + "valor_unitario" DECIMAL(12,2) NOT NULL, + "valor_total" DECIMAL(12,2) NOT NULL, + + CONSTRAINT "sale_items_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "sale_payments" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "sale_id" UUID NOT NULL, + "numero_parcela" SMALLINT NOT NULL DEFAULT 1, + "valor" DECIMAL(12,2) NOT NULL, + "vencimento" DATE NOT NULL, + "pago_em" TIMESTAMPTZ(6), + "forma_pagamento" VARCHAR(30), + "status" "parcela_status" NOT NULL DEFAULT 'pendente', + "alerta_enviado_em" TIMESTAMPTZ(6), + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "sale_payments_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "audit_logs" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "tenant_id" UUID, + "usuario_tipo" VARCHAR(20) NOT NULL, + "usuario_id" UUID, + "acao" VARCHAR(100) NOT NULL, + "entidade" VARCHAR(50), + "entidade_id" UUID, + "detalhes" JSONB, + "ip_origem" INET, + "criado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "master_admins_email_key" ON "master_admins"("email"); + +-- CreateIndex +CREATE INDEX "idx_tenants_status" ON "tenants"("status"); + +-- CreateIndex +CREATE UNIQUE INDEX "tenant_faturas_tenant_id_referencia_key" ON "tenant_faturas"("tenant_id", "referencia"); + +-- CreateIndex +CREATE INDEX "idx_tenant_users_tenant" ON "tenant_users"("tenant_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "tenant_users_tenant_id_email_key" ON "tenant_users"("tenant_id", "email"); + +-- CreateIndex +CREATE INDEX "idx_refresh_tokens_usuario" ON "auth_refresh_tokens"("usuario_tipo", "usuario_id"); + +-- CreateIndex +CREATE INDEX "idx_login_attempts_email_data" ON "login_attempts"("email", "criado_em"); + +-- CreateIndex +CREATE UNIQUE INDEX "tenant_whatsapp_instances_instance_name_key" ON "tenant_whatsapp_instances"("instance_name"); + +-- CreateIndex +CREATE UNIQUE INDEX "tenant_email_config_tenant_id_key" ON "tenant_email_config"("tenant_id"); + +-- CreateIndex +CREATE INDEX "idx_alert_logs_tenant" ON "alert_logs"("tenant_id", "enviado_em"); + +-- CreateIndex +CREATE INDEX "idx_customers_tenant" ON "customers"("tenant_id"); + +-- CreateIndex +CREATE INDEX "idx_products_tenant" ON "products_services"("tenant_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "products_services_tenant_id_sku_key" ON "products_services"("tenant_id", "sku"); + +-- CreateIndex +CREATE INDEX "idx_sales_tenant_data" ON "sales"("tenant_id", "data_venda"); + +-- CreateIndex +CREATE INDEX "idx_sales_customer" ON "sales"("customer_id"); + +-- CreateIndex +CREATE INDEX "idx_sale_items_sale" ON "sale_items"("sale_id"); + +-- CreateIndex +CREATE INDEX "idx_audit_tenant_data" ON "audit_logs"("tenant_id", "criado_em"); + +-- AddForeignKey +ALTER TABLE "tenants" ADD CONSTRAINT "tenants_plano_id_fkey" FOREIGN KEY ("plano_id") REFERENCES "planos"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tenants" ADD CONSTRAINT "tenants_criado_por_fkey" FOREIGN KEY ("criado_por") REFERENCES "master_admins"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tenant_faturas" ADD CONSTRAINT "tenant_faturas_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tenant_users" ADD CONSTRAINT "tenant_users_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tenant_whatsapp_instances" ADD CONSTRAINT "tenant_whatsapp_instances_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tenant_whatsapp_instances" ADD CONSTRAINT "tenant_whatsapp_instances_evolution_config_id_fkey" FOREIGN KEY ("evolution_config_id") REFERENCES "evolution_api_config"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tenant_email_config" ADD CONSTRAINT "tenant_email_config_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "alert_templates" ADD CONSTRAINT "alert_templates_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "alert_logs" ADD CONSTRAINT "alert_logs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "alert_logs" ADD CONSTRAINT "alert_logs_template_id_fkey" FOREIGN KEY ("template_id") REFERENCES "alert_templates"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "customers" ADD CONSTRAINT "customers_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "customer_contacts" ADD CONSTRAINT "customer_contacts_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "products_services" ADD CONSTRAINT "products_services_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "sales" ADD CONSTRAINT "sales_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "sales" ADD CONSTRAINT "sales_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "sales" ADD CONSTRAINT "sales_vendedor_id_fkey" FOREIGN KEY ("vendedor_id") REFERENCES "tenant_users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "sale_items" ADD CONSTRAINT "sale_items_sale_id_fkey" FOREIGN KEY ("sale_id") REFERENCES "sales"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "sale_items" ADD CONSTRAINT "sale_items_product_service_id_fkey" FOREIGN KEY ("product_service_id") REFERENCES "products_services"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "sale_payments" ADD CONSTRAINT "sale_payments_sale_id_fkey" FOREIGN KEY ("sale_id") REFERENCES "sales"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260812161100_rls_views_constraints/migration.sql b/prisma/migrations/20260812161100_rls_views_constraints/migration.sql new file mode 100644 index 0000000..e14d005 --- /dev/null +++ b/prisma/migrations/20260812161100_rls_views_constraints/migration.sql @@ -0,0 +1,119 @@ +-- Migration manual: construtos que o Prisma DSL não expressa nativamente. +-- Fonte de verdade: Etapas/seconectai-vendas-schema.sql +-- Contém: extensão pgcrypto, CHECK constraint, índices parciais, views de +-- dashboard e Row Level Security (ENABLE + CREATE POLICY) para isolamento +-- entre tenants. + +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + +-- ============================================================ +-- CHECK constraints +-- ============================================================ + +ALTER TABLE "tenants" + ADD CONSTRAINT "tenants_dia_vencimento_mensalidade_check" + CHECK ("dia_vencimento_mensalidade" BETWEEN 1 AND 28); + +-- ============================================================ +-- Índices parciais +-- ============================================================ + +CREATE INDEX "idx_tenant_faturas_venc" + ON "tenant_faturas"("vencimento") + WHERE "status" IN ('pendente', 'atrasado'); + +CREATE INDEX "idx_sale_payments_venc" + ON "sale_payments"("vencimento") + WHERE "status" IN ('pendente', 'atrasado'); + +-- ============================================================ +-- Views de dashboard (painel do tenant) +-- ============================================================ + +CREATE VIEW "vw_resumo_vendas_semanal" AS +SELECT + s.tenant_id, + date_trunc('week', s.data_venda)::date AS semana, + COUNT(*) AS qtd_vendas, + SUM(s.valor_total) AS total_vendido +FROM sales s +WHERE s.status IN ('confirmada', 'concluida') +GROUP BY s.tenant_id, date_trunc('week', s.data_venda); + +CREATE VIEW "vw_resumo_vendas_mensal" AS +SELECT + s.tenant_id, + date_trunc('month', s.data_venda)::date AS mes, + COUNT(*) AS qtd_vendas, + SUM(s.valor_total) AS total_vendido +FROM sales s +WHERE s.status IN ('confirmada', 'concluida') +GROUP BY s.tenant_id, date_trunc('month', s.data_venda); + +CREATE VIEW "vw_recebimentos" AS +SELECT + sp.id, + s.tenant_id, + s.customer_id, + sp.valor, + sp.vencimento, + sp.status, + sp.pago_em +FROM sale_payments sp +JOIN sales s ON s.id = sp.sale_id; + +-- ============================================================ +-- Row Level Security — isolamento entre tenants +-- ============================================================ +-- A aplicação deve definir, por conexão/transação: +-- SET app.current_tenant_id = ''; +-- Usuários master (que enxergam todos os tenants) usam uma role de banco +-- separada (ex: app_master) que faz BYPASS RLS, ou uma policy adicional +-- que libera quando app.is_master = 'true'. Implementado na Etapa 2. + +ALTER TABLE "customers" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "customer_contacts" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "products_services" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "sales" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "sale_items" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "sale_payments" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "tenant_users" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "tenant_whatsapp_instances" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "tenant_email_config" ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "tenant_isolation_customers" ON "customers" + USING ("tenant_id" = current_setting('app.current_tenant_id', true)::uuid); + +CREATE POLICY "tenant_isolation_products" ON "products_services" + USING ("tenant_id" = current_setting('app.current_tenant_id', true)::uuid); + +CREATE POLICY "tenant_isolation_sales" ON "sales" + USING ("tenant_id" = current_setting('app.current_tenant_id', true)::uuid); + +CREATE POLICY "tenant_isolation_tenant_users" ON "tenant_users" + USING ("tenant_id" = current_setting('app.current_tenant_id', true)::uuid); + +CREATE POLICY "tenant_isolation_whatsapp" ON "tenant_whatsapp_instances" + USING ("tenant_id" = current_setting('app.current_tenant_id', true)::uuid); + +CREATE POLICY "tenant_isolation_email_config" ON "tenant_email_config" + USING ("tenant_id" = current_setting('app.current_tenant_id', true)::uuid); + +-- sale_items e customer_contacts não têm tenant_id direto: isolar via join +CREATE POLICY "tenant_isolation_sale_items" ON "sale_items" + USING ("sale_id" IN ( + SELECT id FROM sales + WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid + )); + +CREATE POLICY "tenant_isolation_sale_payments" ON "sale_payments" + USING ("sale_id" IN ( + SELECT id FROM sales + WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid + )); + +CREATE POLICY "tenant_isolation_customer_contacts" ON "customer_contacts" + USING ("customer_id" IN ( + SELECT id FROM customers + WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid + )); diff --git a/prisma/migrations/20260813120000_auth_app_roles/migration.sql b/prisma/migrations/20260813120000_auth_app_roles/migration.sql new file mode 100644 index 0000000..3b4f55f --- /dev/null +++ b/prisma/migrations/20260813120000_auth_app_roles/migration.sql @@ -0,0 +1,53 @@ +-- Migration manual: roles de aplicação usadas pela Etapa 2 (Autenticação/RLS). +-- +-- Por que duas roles novas em vez de reaproveitar a role do DATABASE_URL +-- (`seconectai`, dono das tabelas): a role criada pelo `POSTGRES_USER` do +-- docker-compose é a role bootstrap do initdb, portanto é SUPERUSER — e +-- superusers do Postgres **sempre** ignoram Row Level Security, mesmo com +-- `ENABLE ROW LEVEL SECURITY`/`FORCE ROW LEVEL SECURITY` nas tabelas. Se a +-- aplicação continuasse consultando o banco com essa role em runtime, o +-- isolamento por tenant seria só decorativo. Por isso: +-- +-- * `app_master` — LOGIN + BYPASSRLS, mas SEM ser superuser. Usada via +-- `DATABASE_URL_MASTER` pelo painel master (enxerga todos os tenants), +-- como pedido no prompt da etapa. +-- * `app_tenant` — LOGIN comum, SEM BYPASSRLS e SEM ser superuser. Usada +-- via `DATABASE_URL_TENANT` para toda query autenticada como +-- tenant_user, dentro da transação que executa +-- `SET LOCAL app.current_tenant_id`; é essa role que faz as policies de +-- RLS (`prisma/migrations/20260812161100_rls_views_constraints`) valerem +-- de fato. (Desvio registrado em Etapas/09-controle-de-progresso.md: o +-- prompt original só citava `DATABASE_URL_MASTER`.) +-- +-- `DATABASE_URL` (role `seconectai`) continua sendo usada só para +-- migrations e para o `PrismaService` "admin" (health check, bootstrap de +-- login antes de existir um tenant_id de sessão — ver AuthService). +-- +-- As senhas abaixo são placeholders de desenvolvimento, no mesmo espírito +-- de `POSTGRES_PASSWORD`/`JWT_SECRET` em `.env.example`: troque-as em +-- produção com `ALTER ROLE ... WITH PASSWORD '...'` e atualize +-- `DATABASE_URL_MASTER`/`DATABASE_URL_TENANT` de acordo. + +DO $$ +BEGIN + IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'app_master') THEN + CREATE ROLE app_master WITH LOGIN PASSWORD 'app_master_changeme' NOSUPERUSER BYPASSRLS; + END IF; + + IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'app_tenant') THEN + CREATE ROLE app_tenant WITH LOGIN PASSWORD 'app_tenant_changeme' NOSUPERUSER NOBYPASSRLS; + END IF; +END +$$; + +GRANT USAGE ON SCHEMA public TO app_master, app_tenant; + +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_master, app_tenant; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_master, app_tenant; + +-- Cobre tabelas/sequences criadas por migrations futuras (etapas 3+), +-- desde que aplicadas com a mesma role que rodou esta migration. +ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_master, app_tenant; +ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT USAGE, SELECT ON SEQUENCES TO app_master, app_tenant; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..2eee65b --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,438 @@ +// SeConectai Vendas — Prisma schema +// Traduzido 1:1 a partir de `Etapas/seconectai-vendas-schema.sql` (fonte de verdade). +// Constructos que o Prisma DSL não expressa nativamente (views, índices parciais, +// CHECK constraints e as policies de RLS) são criados por uma migration manual +// em `prisma/migrations/_rls_views_constraints/migration.sql`. + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// ============================================================ +// ENUMS +// ============================================================ + +enum tenant_status { + trial + ativo + inadimplente + suspenso + cancelado +} + +enum cobranca_status { + pendente + pago + atrasado + cancelado +} + +enum user_role { + tenant_admin + vendedor + financeiro + visualizador +} + +enum item_tipo { + produto + servico +} + +enum venda_status { + orcamento + confirmada + cancelada + concluida +} + +enum parcela_status { + pendente + pago + atrasado + cancelado +} + +// ============================================================ +// 1. NÍVEL MASTER +// ============================================================ + +model master_admins { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + nome String @db.VarChar(150) + email String @unique @db.VarChar(150) + senha_hash String + ativo Boolean @default(true) + mfa_secret String? + ultimo_login DateTime? @db.Timestamptz(6) + criado_em DateTime @default(now()) @db.Timestamptz(6) + atualizado_em DateTime @default(now()) @db.Timestamptz(6) + + tenants_criados tenants[] +} + +model evolution_api_config { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + nome String @db.VarChar(100) + base_url String @db.VarChar(255) + api_key_cripto String + versao String @default("2.3") @db.VarChar(20) + ativo Boolean @default(true) + criado_em DateTime @default(now()) @db.Timestamptz(6) + + instancias tenant_whatsapp_instances[] +} + +model master_email_config { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + smtp_host String @db.VarChar(150) + smtp_porta Int + smtp_usuario String @db.VarChar(150) + smtp_senha_cripto String + email_remetente String @db.VarChar(150) + ativo Boolean @default(true) + criado_em DateTime @default(now()) @db.Timestamptz(6) +} + +model planos { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + nome String @db.VarChar(100) + descricao String? + valor_mensal Decimal @db.Decimal(10, 2) + limite_customers Int? + limite_usuarios Int? + limite_instancias_whatsapp Int @default(1) + ativo Boolean @default(true) + criado_em DateTime @default(now()) @db.Timestamptz(6) + + tenants tenants[] +} + +// ============================================================ +// 2. TENANTS +// ============================================================ + +model tenants { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + razao_social String @db.VarChar(200) + nome_fantasia String? @db.VarChar(200) + documento String? @db.VarChar(20) + email_contato String @db.VarChar(150) + telefone String? @db.VarChar(20) + logo_url String? + cor_primaria String? @db.VarChar(7) + plano_id String? @db.Uuid + status tenant_status @default(trial) + dia_vencimento_mensalidade Int @default(10) @db.SmallInt + trial_expira_em DateTime? @db.Timestamptz(6) + criado_por String? @db.Uuid + criado_em DateTime @default(now()) @db.Timestamptz(6) + atualizado_em DateTime @default(now()) @db.Timestamptz(6) + + plano planos? @relation(fields: [plano_id], references: [id]) + criador master_admins? @relation(fields: [criado_por], references: [id]) + + faturas tenant_faturas[] + usuarios tenant_users[] + whatsapp_instances tenant_whatsapp_instances[] + email_config tenant_email_config? + alert_templates alert_templates[] + alert_logs alert_logs[] + customers customers[] + produtos products_services[] + vendas sales[] + audit_logs audit_logs[] + + @@index([status], map: "idx_tenants_status") +} + +model tenant_faturas { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + tenant_id String @db.Uuid + referencia String @db.VarChar(7) + valor Decimal @db.Decimal(10, 2) + vencimento DateTime @db.Date + pago_em DateTime? @db.Timestamptz(6) + status cobranca_status @default(pendente) + alerta_enviado_em DateTime? @db.Timestamptz(6) + criado_em DateTime @default(now()) @db.Timestamptz(6) + + tenant tenants @relation(fields: [tenant_id], references: [id], onDelete: Cascade) + + @@unique([tenant_id, referencia]) +} + +// ============================================================ +// 3. USUÁRIOS E AUTENTICAÇÃO +// ============================================================ + +model tenant_users { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + tenant_id String @db.Uuid + nome String @db.VarChar(150) + email String @db.VarChar(150) + senha_hash String + role user_role @default(vendedor) + ativo Boolean @default(true) + mfa_secret String? + ultimo_login DateTime? @db.Timestamptz(6) + criado_em DateTime @default(now()) @db.Timestamptz(6) + + tenant tenants @relation(fields: [tenant_id], references: [id], onDelete: Cascade) + + vendas sales[] + + @@unique([tenant_id, email]) + @@index([tenant_id], map: "idx_tenant_users_tenant") +} + +model auth_refresh_tokens { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + usuario_tipo String @db.VarChar(20) + usuario_id String @db.Uuid + token_hash String + expira_em DateTime @db.Timestamptz(6) + revogado Boolean @default(false) + ip_origem String? @db.Inet + user_agent String? + criado_em DateTime @default(now()) @db.Timestamptz(6) + + @@index([usuario_tipo, usuario_id], map: "idx_refresh_tokens_usuario") +} + +model password_reset_tokens { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + usuario_tipo String @db.VarChar(20) + usuario_id String @db.Uuid + token_hash String + expira_em DateTime @db.Timestamptz(6) + usado Boolean @default(false) + criado_em DateTime @default(now()) @db.Timestamptz(6) +} + +model login_attempts { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + email String @db.VarChar(150) + usuario_tipo String @db.VarChar(20) + sucesso Boolean + ip_origem String? @db.Inet + criado_em DateTime @default(now()) @db.Timestamptz(6) + + @@index([email, criado_em], map: "idx_login_attempts_email_data") +} + +// ============================================================ +// 4. INTEGRAÇÕES POR TENANT +// ============================================================ + +model tenant_whatsapp_instances { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + tenant_id String @db.Uuid + evolution_config_id String? @db.Uuid + instance_name String @unique @db.VarChar(100) + numero_conectado String? @db.VarChar(20) + status String @default("desconectado") @db.VarChar(30) + qr_code_gerado_em DateTime? @db.Timestamptz(6) + criado_em DateTime @default(now()) @db.Timestamptz(6) + + tenant tenants @relation(fields: [tenant_id], references: [id], onDelete: Cascade) + evolution_config evolution_api_config? @relation(fields: [evolution_config_id], references: [id]) +} + +model tenant_email_config { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + tenant_id String @unique @db.Uuid + smtp_host String? @db.VarChar(150) + smtp_porta Int? + smtp_usuario String? @db.VarChar(150) + smtp_senha_cripto String? + email_remetente String? @db.VarChar(150) + usar_config_padrao Boolean @default(true) + criado_em DateTime @default(now()) @db.Timestamptz(6) + + tenant tenants @relation(fields: [tenant_id], references: [id], onDelete: Cascade) +} + +model alert_templates { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + tenant_id String? @db.Uuid + tipo String @db.VarChar(50) + canal String @db.VarChar(20) + assunto String? @db.VarChar(200) + corpo String + ativo Boolean @default(true) + criado_em DateTime @default(now()) @db.Timestamptz(6) + + tenant tenants? @relation(fields: [tenant_id], references: [id], onDelete: Cascade) + + alert_logs alert_logs[] +} + +model alert_logs { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + tenant_id String? @db.Uuid + destinatario_tipo String @db.VarChar(20) + destinatario_id String @db.Uuid + canal String @db.VarChar(20) + template_id String? @db.Uuid + conteudo_enviado String? + status String @default("enviado") @db.VarChar(20) + erro String? + enviado_em DateTime @default(now()) @db.Timestamptz(6) + + tenant tenants? @relation(fields: [tenant_id], references: [id], onDelete: Cascade) + template alert_templates? @relation(fields: [template_id], references: [id]) + + @@index([tenant_id, enviado_em], map: "idx_alert_logs_tenant") +} + +// ============================================================ +// 5. CLIENTES DO TENANT ("customers") +// ============================================================ + +model customers { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + tenant_id String @db.Uuid + razao_social String @db.VarChar(200) + nome_fantasia String? @db.VarChar(200) + documento String? @db.VarChar(20) + email String? @db.VarChar(150) + telefone_whatsapp String? @db.VarChar(20) + endereco String? + cidade String? @db.VarChar(100) + uf String? @db.Char(2) + observacoes String? + ativo Boolean @default(true) + criado_em DateTime @default(now()) @db.Timestamptz(6) + atualizado_em DateTime @default(now()) @db.Timestamptz(6) + + tenant tenants @relation(fields: [tenant_id], references: [id], onDelete: Cascade) + contatos customer_contacts[] + vendas sales[] + + @@index([tenant_id], map: "idx_customers_tenant") +} + +model customer_contacts { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + customer_id String @db.Uuid + nome String @db.VarChar(150) + cargo String? @db.VarChar(100) + email String? @db.VarChar(150) + telefone_whatsapp String? @db.VarChar(20) + principal Boolean @default(false) + criado_em DateTime @default(now()) @db.Timestamptz(6) + + customer customers @relation(fields: [customer_id], references: [id], onDelete: Cascade) +} + +// ============================================================ +// 6. PRODUTOS E SERVIÇOS DO TENANT +// ============================================================ + +model products_services { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + tenant_id String @db.Uuid + tipo item_tipo + nome String @db.VarChar(200) + descricao String? + sku String? @db.VarChar(50) + valor_venda Decimal @db.Decimal(12, 2) + valor_custo Decimal? @db.Decimal(12, 2) + unidade String @default("un") @db.VarChar(20) + recorrente Boolean @default(false) + ativo Boolean @default(true) + criado_em DateTime @default(now()) @db.Timestamptz(6) + atualizado_em DateTime @default(now()) @db.Timestamptz(6) + + tenant tenants @relation(fields: [tenant_id], references: [id], onDelete: Cascade) + + itens_venda sale_items[] + + @@unique([tenant_id, sku]) + @@index([tenant_id], map: "idx_products_tenant") +} + +// ============================================================ +// 7. VENDAS +// ============================================================ + +model sales { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + tenant_id String @db.Uuid + customer_id String @db.Uuid + vendedor_id String? @db.Uuid + numero_venda Int @default(autoincrement()) + status venda_status @default(confirmada) + valor_total Decimal @default(0) @db.Decimal(12, 2) + desconto Decimal @default(0) @db.Decimal(12, 2) + observacoes String? + data_venda DateTime @default(now()) @db.Date + criado_em DateTime @default(now()) @db.Timestamptz(6) + atualizado_em DateTime @default(now()) @db.Timestamptz(6) + + tenant tenants @relation(fields: [tenant_id], references: [id], onDelete: Cascade) + customer customers @relation(fields: [customer_id], references: [id]) + vendedor tenant_users? @relation(fields: [vendedor_id], references: [id]) + + itens sale_items[] + parcelas sale_payments[] + + @@index([tenant_id, data_venda], map: "idx_sales_tenant_data") + @@index([customer_id], map: "idx_sales_customer") +} + +model sale_items { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + sale_id String @db.Uuid + product_service_id String @db.Uuid + quantidade Decimal @default(1) @db.Decimal(10, 2) + valor_unitario Decimal @db.Decimal(12, 2) + valor_total Decimal @db.Decimal(12, 2) + + sale sales @relation(fields: [sale_id], references: [id], onDelete: Cascade) + produto products_services @relation(fields: [product_service_id], references: [id]) + + @@index([sale_id], map: "idx_sale_items_sale") +} + +model sale_payments { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + sale_id String @db.Uuid + numero_parcela Int @default(1) @db.SmallInt + valor Decimal @db.Decimal(12, 2) + vencimento DateTime @db.Date + pago_em DateTime? @db.Timestamptz(6) + forma_pagamento String? @db.VarChar(30) + status parcela_status @default(pendente) + alerta_enviado_em DateTime? @db.Timestamptz(6) + criado_em DateTime @default(now()) @db.Timestamptz(6) + + sale sales @relation(fields: [sale_id], references: [id], onDelete: Cascade) +} + +// ============================================================ +// 8. AUDITORIA GERAL +// ============================================================ + +model audit_logs { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + tenant_id String? @db.Uuid + usuario_tipo String @db.VarChar(20) + usuario_id String? @db.Uuid + acao String @db.VarChar(100) + entidade String? @db.VarChar(50) + entidade_id String? @db.Uuid + detalhes Json? + ip_origem String? @db.Inet + criado_em DateTime @default(now()) @db.Timestamptz(6) + + tenant tenants? @relation(fields: [tenant_id], references: [id], onDelete: SetNull) + + @@index([tenant_id, criado_em], map: "idx_audit_tenant_data") +} diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100644 index 0000000..bfc0ccd --- /dev/null +++ b/src/app.module.ts @@ -0,0 +1,37 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { APP_GUARD } from '@nestjs/core'; +import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; +import { EncryptionModule } from './common/encryption.module'; +import { validate } from './config/env.validation'; +import { PrismaModule } from './config/prisma.module'; +import { AlertDispatchModule } from './modules/alert-dispatch/alert-dispatch.module'; +import { AuthModule } from './modules/auth/auth.module'; +import { EmailConfigModule } from './modules/email-config/email-config.module'; +import { EvolutionModule } from './modules/evolution/evolution.module'; +import { HealthModule } from './modules/health/health.module'; +import { PlanosModule } from './modules/planos/planos.module'; +import { TenantsModule } from './modules/tenants/tenants.module'; +import { WhatsappModule } from './modules/whatsapp/whatsapp.module'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + validate, + }), + ThrottlerModule.forRoot([{ name: 'default', ttl: 60_000, limit: 60 }]), + PrismaModule, + EncryptionModule, + AuthModule, + HealthModule, + PlanosModule, + TenantsModule, + EvolutionModule, + AlertDispatchModule, + EmailConfigModule, + WhatsappModule, + ], + providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }], +}) +export class AppModule {} diff --git a/src/common/audit-log.util.ts b/src/common/audit-log.util.ts new file mode 100644 index 0000000..befdf5b --- /dev/null +++ b/src/common/audit-log.util.ts @@ -0,0 +1,19 @@ +import { Prisma, PrismaClient } from '@prisma/client'; + +type AuditableClient = PrismaClient | Prisma.TransactionClient; + +export interface AuditLogParams { + usuario_tipo: 'master' | 'tenant_user'; + usuario_id?: string | null; + tenant_id?: string | null; + acao: string; + entidade?: string; + entidade_id?: string | null; + detalhes?: Prisma.InputJsonValue; + ip_origem?: string; +} + +/** Grava uma linha em `audit_logs`. Passe o `tx` da transação em andamento para que a auditoria só valha se o resto da operação for confirmado junto. */ +export function writeAuditLog(client: AuditableClient, params: AuditLogParams) { + return client.audit_logs.create({ data: params }); +} diff --git a/src/common/encryption.module.ts b/src/common/encryption.module.ts new file mode 100644 index 0000000..7502223 --- /dev/null +++ b/src/common/encryption.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { EncryptionService } from './encryption.service'; + +@Global() +@Module({ + providers: [EncryptionService], + exports: [EncryptionService], +}) +export class EncryptionModule {} diff --git a/src/common/encryption.service.spec.ts b/src/common/encryption.service.spec.ts new file mode 100644 index 0000000..5f1b833 --- /dev/null +++ b/src/common/encryption.service.spec.ts @@ -0,0 +1,34 @@ +import { ConfigService } from '@nestjs/config'; +import { EncryptionService } from './encryption.service'; + +describe('EncryptionService', () => { + const buildService = (key = 'chave-de-teste-com-mais-de-32-caracteres') => + new EncryptionService({ get: () => key } as unknown as ConfigService); + + it('decifra um valor cifrado de volta ao texto original', () => { + const service = buildService(); + const cifrado = service.encrypt('minha-api-key-secreta'); + + expect(cifrado).not.toBe('minha-api-key-secreta'); + expect(service.decrypt(cifrado)).toBe('minha-api-key-secreta'); + }); + + it('gera saídas diferentes para o mesmo texto (IV aleatório)', () => { + const service = buildService(); + + expect(service.encrypt('mesmo-valor')).not.toBe(service.encrypt('mesmo-valor')); + }); + + it('falha ao decifrar com uma chave diferente da usada para cifrar', () => { + const cifrado = buildService('chave-a-com-mais-de-32-caracteres').encrypt('segredo'); + const outraChave = buildService('chave-b-com-mais-de-32-caracteres-tambem'); + + expect(() => outraChave.decrypt(cifrado)).toThrow(); + }); + + it('lança erro na inicialização se ENCRYPTION_KEY não estiver definida', () => { + expect( + () => new EncryptionService({ get: () => undefined } as unknown as ConfigService), + ).toThrow(/ENCRYPTION_KEY/); + }); +}); diff --git a/src/common/encryption.service.ts b/src/common/encryption.service.ts new file mode 100644 index 0000000..947cd63 --- /dev/null +++ b/src/common/encryption.service.ts @@ -0,0 +1,52 @@ +import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +const ALGORITHM = 'aes-256-gcm'; +const IV_LENGTH = 12; + +/** + * Cifra/decifra segredos em repouso (api_key da Evolution API, senha SMTP) + * com AES-256-GCM. A chave vem de `ENCRYPTION_KEY` (env, nunca hardcoded) e + * é reduzida a 32 bytes via SHA-256 — assim `ENCRYPTION_KEY` pode ser + * qualquer string longa o suficiente (mesmo padrão de validação de + * `JWT_SECRET`), sem exigir um valor hex/base64 de tamanho exato. + */ +@Injectable() +export class EncryptionService { + private readonly key: Buffer; + + constructor(configService: ConfigService) { + const secret = configService.get('ENCRYPTION_KEY'); + if (!secret) { + throw new Error( + 'ENCRYPTION_KEY não definida. Ela é obrigatória para cifrar segredos como api_key ' + + 'da Evolution API e senha SMTP (ver .env.example).', + ); + } + this.key = createHash('sha256').update(secret).digest(); + } + + /** Retorna `iv:authTag:conteudo` (hex), pronto para gravar em uma coluna `*_cripto`. */ + encrypt(plainText: string): string { + const iv = randomBytes(IV_LENGTH); + const cipher = createCipheriv(ALGORITHM, this.key, iv); + const encrypted = Buffer.concat([cipher.update(plainText, 'utf8'), cipher.final()]); + const authTag = cipher.getAuthTag(); + return [iv, authTag, encrypted].map((buffer) => buffer.toString('hex')).join(':'); + } + + decrypt(cipherText: string): string { + const [ivHex, authTagHex, dataHex] = cipherText.split(':'); + if (!ivHex || !authTagHex || !dataHex) { + throw new Error('Formato de valor cifrado inválido'); + } + + const decipher = createDecipheriv(ALGORITHM, this.key, Buffer.from(ivHex, 'hex')); + decipher.setAuthTag(Buffer.from(authTagHex, 'hex')); + + return Buffer.concat([decipher.update(Buffer.from(dataHex, 'hex')), decipher.final()]).toString( + 'utf8', + ); + } +} diff --git a/src/common/mask.util.ts b/src/common/mask.util.ts new file mode 100644 index 0000000..7622a39 --- /dev/null +++ b/src/common/mask.util.ts @@ -0,0 +1,10 @@ +/** + * Mascara um segredo em texto puro para exibição em resposta de API: nunca + * revela o tamanho real do valor (largura fixa de asteriscos) e mostra só + * os últimos 4 caracteres, para o usuário reconhecer qual segredo é sem + * conseguir reconstruí-lo. + */ +export function maskSecret(plainText: string): string { + const ultimosQuatro = plainText.slice(-4); + return `${'*'.repeat(8)}${ultimosQuatro}`; +} diff --git a/src/config/env.validation.ts b/src/config/env.validation.ts new file mode 100644 index 0000000..296d6fa --- /dev/null +++ b/src/config/env.validation.ts @@ -0,0 +1,76 @@ +import { plainToInstance } from 'class-transformer'; +import { IsIn, IsInt, IsOptional, IsString, MinLength, validateSync } from 'class-validator'; + +class EnvironmentVariables { + @IsIn(['development', 'production', 'test']) + @IsOptional() + NODE_ENV?: string; + + @IsInt() + @IsOptional() + PORT?: number; + + @IsString() + DATABASE_URL: string; + + @IsString() + @IsOptional() + DATABASE_URL_MASTER?: string; + + @IsString() + @IsOptional() + DATABASE_URL_TENANT?: string; + + @IsString() + @MinLength(32) + JWT_SECRET: string; + + @IsString() + @MinLength(32) + ENCRYPTION_KEY: string; + + @IsString() + @IsOptional() + REDIS_HOST?: string; + + @IsInt() + @IsOptional() + REDIS_PORT?: number; + + @IsString() + @IsOptional() + JWT_ACCESS_EXPIRES_IN?: string; + + @IsInt() + @IsOptional() + REFRESH_TOKEN_EXPIRES_IN_DAYS?: number; + + @IsInt() + @IsOptional() + PASSWORD_RESET_EXPIRES_IN_HOURS?: number; + + @IsInt() + @IsOptional() + LOGIN_MAX_ATTEMPTS?: number; + + @IsInt() + @IsOptional() + LOGIN_LOCKOUT_WINDOW_MINUTES?: number; + + @IsInt() + @IsOptional() + TENANT_TRIAL_DIAS?: number; +} + +export function validate(config: Record) { + const validatedConfig = plainToInstance(EnvironmentVariables, config, { + enableImplicitConversion: true, + }); + const errors = validateSync(validatedConfig, { skipMissingProperties: false }); + + if (errors.length > 0) { + throw new Error(errors.toString()); + } + + return validatedConfig; +} diff --git a/src/config/prisma-master.service.ts b/src/config/prisma-master.service.ts new file mode 100644 index 0000000..307d072 --- /dev/null +++ b/src/config/prisma-master.service.ts @@ -0,0 +1,27 @@ +import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PrismaClient } from '@prisma/client'; + +/** + * Conectada via `DATABASE_URL_MASTER` (role `app_master`, LOGIN + BYPASSRLS). + * Usada pelo painel master, que precisa enxergar dados de todos os tenants + * sem passar pelas policies de RLS. + */ +@Injectable() +export class PrismaMasterService extends PrismaClient implements OnModuleInit, OnModuleDestroy { + constructor(configService: ConfigService) { + super({ + datasourceUrl: + configService.get('DATABASE_URL_MASTER') ?? + configService.get('DATABASE_URL'), + }); + } + + async onModuleInit() { + await this.$connect(); + } + + async onModuleDestroy() { + await this.$disconnect(); + } +} diff --git a/src/config/prisma-tenant.service.ts b/src/config/prisma-tenant.service.ts new file mode 100644 index 0000000..3e4abd9 --- /dev/null +++ b/src/config/prisma-tenant.service.ts @@ -0,0 +1,32 @@ +import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PrismaClient } from '@prisma/client'; + +/** + * Conectada via `DATABASE_URL_TENANT` (role `app_tenant`, sem BYPASSRLS). + * Toda query de request autenticada como tenant_user deve passar por essa + * conexão, dentro de uma transação com `SET LOCAL app.current_tenant_id` + * (ver `TenantContextService`) — é essa role não-superuser que faz as + * policies de RLS valerem de fato. + */ +@Injectable() +export class PrismaTenantService extends PrismaClient implements OnModuleInit, OnModuleDestroy { + constructor(configService: ConfigService) { + const url = configService.get('DATABASE_URL_TENANT'); + if (!url) { + throw new Error( + 'DATABASE_URL_TENANT não definida. Ela é obrigatória: sem ela, o isolamento por ' + + 'tenant via Row Level Security não é aplicado (ver .env.example).', + ); + } + super({ datasourceUrl: url }); + } + + async onModuleInit() { + await this.$connect(); + } + + async onModuleDestroy() { + await this.$disconnect(); + } +} diff --git a/src/config/prisma.module.ts b/src/config/prisma.module.ts new file mode 100644 index 0000000..d77c1b8 --- /dev/null +++ b/src/config/prisma.module.ts @@ -0,0 +1,12 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaMasterService } from './prisma-master.service'; +import { PrismaTenantService } from './prisma-tenant.service'; +import { PrismaService } from './prisma.service'; +import { TenantContextService } from './tenant-context.service'; + +@Global() +@Module({ + providers: [PrismaService, PrismaMasterService, PrismaTenantService, TenantContextService], + exports: [PrismaService, PrismaMasterService, PrismaTenantService, TenantContextService], +}) +export class PrismaModule {} diff --git a/src/config/prisma.service.ts b/src/config/prisma.service.ts new file mode 100644 index 0000000..623d5e0 --- /dev/null +++ b/src/config/prisma.service.ts @@ -0,0 +1,13 @@ +import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { + async onModuleInit() { + await this.$connect(); + } + + async onModuleDestroy() { + await this.$disconnect(); + } +} diff --git a/src/config/tenant-context.service.ts b/src/config/tenant-context.service.ts new file mode 100644 index 0000000..e79a231 --- /dev/null +++ b/src/config/tenant-context.service.ts @@ -0,0 +1,49 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; +import { Injectable } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaTenantService } from './prisma-tenant.service'; + +interface TenantStore { + tenantId: string; + tx: Prisma.TransactionClient; +} + +/** + * Implementa o mecanismo de RLS por request: abre uma transação na conexão + * `app_tenant` (sem BYPASSRLS), executa `SET LOCAL app.current_tenant_id` + * dentro dela via `set_config(..., true)`, e disponibiliza essa transação + * para o resto do request via AsyncLocalStorage — nunca a partir de um + * `tenant_id` vindo de input do usuário, sempre do `tenant_id` já resolvido + * pelo token (ver `TenantRlsInterceptor`). + */ +@Injectable() +export class TenantContextService { + private readonly als = new AsyncLocalStorage(); + + constructor(private readonly prismaTenant: PrismaTenantService) {} + + async run(tenantId: string, fn: () => Promise): Promise { + return this.prismaTenant.$transaction(async (tx) => { + await tx.$executeRaw`SELECT set_config('app.current_tenant_id', ${tenantId}, true)`; + return this.als.run({ tenantId, tx }, fn); + }); + } + + getTx(): Prisma.TransactionClient { + const store = this.als.getStore(); + if (!store) { + throw new Error('TenantContextService.getTx() chamado fora de um contexto de tenant ativo'); + } + return store.tx; + } + + getTenantId(): string { + const store = this.als.getStore(); + if (!store) { + throw new Error( + 'TenantContextService.getTenantId() chamado fora de um contexto de tenant ativo', + ); + } + return store.tenantId; + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..da4fb08 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,22 @@ +import { ValidationPipe } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { ConfigService } from '@nestjs/config'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + const configService = app.get(ConfigService); + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + ); + + const port = configService.get('PORT', 3000); + await app.listen(port); +} + +void bootstrap(); diff --git a/src/modules/alert-dispatch/alert-dispatch.module.ts b/src/modules/alert-dispatch/alert-dispatch.module.ts new file mode 100644 index 0000000..e1c0b96 --- /dev/null +++ b/src/modules/alert-dispatch/alert-dispatch.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { EmailModule } from '../email-config/email.module'; +import { EvolutionModule } from '../evolution/evolution.module'; +import { AlertDispatchService } from './alert-dispatch.service'; + +@Module({ + imports: [EvolutionModule, EmailModule], + providers: [AlertDispatchService], + exports: [AlertDispatchService], +}) +export class AlertDispatchModule {} diff --git a/src/modules/alert-dispatch/alert-dispatch.service.ts b/src/modules/alert-dispatch/alert-dispatch.service.ts new file mode 100644 index 0000000..304b038 --- /dev/null +++ b/src/modules/alert-dispatch/alert-dispatch.service.ts @@ -0,0 +1,102 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { EncryptionService } from '../../common/encryption.service'; +import { PrismaMasterService } from '../../config/prisma-master.service'; +import { EmailService } from '../email-config/email.service'; +import { EvolutionApiClient } from '../evolution/evolution-api.client'; + +export interface DispatchResult { + sucesso: boolean; + erro?: string; +} + +/** + * Serviço central de envio, preparando a Etapa 7 (motor de alertas): só + * executa o envio de um WhatsApp/e-mail já resolvido, sem nenhuma lógica de + * "quando enviar" — isso fica para o worker BullMQ da Etapa 7. Usa + * `PrismaMasterService` (nunca `TenantContextService`/RLS) porque este + * service precisa funcionar tanto dentro de uma request HTTP (endpoints de + * "testar envio" desta etapa) quanto, futuramente, dentro de um worker de + * fila sem nenhum contexto de request/tenant ativo — por isso todo acesso a + * tabela por-tenant aqui filtra `tenant_id` manualmente. + */ +@Injectable() +export class AlertDispatchService { + constructor( + private readonly prismaMaster: PrismaMasterService, + private readonly encryptionService: EncryptionService, + private readonly evolutionApiClient: EvolutionApiClient, + private readonly emailService: EmailService, + ) {} + + /** + * `instanceId` é opcional: quando omitido (uso futuro pela Etapa 7), + * resolve a instância conectada mais recente do tenant. Quando informado + * (usado pelo endpoint `POST /tenant/whatsapp/instancias/:id/testar-envio`), + * envia especificamente por aquela instância, para validar a conexão dela. + */ + async enviarWhatsapp( + tenantId: string, + numero: string, + mensagem: string, + instanceId?: string, + ): Promise { + try { + const instance = instanceId + ? await this.prismaMaster.tenant_whatsapp_instances.findFirst({ + where: { id: instanceId, tenant_id: tenantId }, + }) + : await this.prismaMaster.tenant_whatsapp_instances.findFirst({ + where: { tenant_id: tenantId, status: 'conectado' }, + orderBy: { criado_em: 'desc' }, + }); + + if (!instance) { + throw new BadRequestException( + instanceId + ? 'Instância de WhatsApp não encontrada' + : 'Nenhuma instância de WhatsApp conectada para este tenant', + ); + } + if (!instance.evolution_config_id) { + throw new BadRequestException( + 'Instância de WhatsApp sem configuração da Evolution API associada', + ); + } + + const config = await this.prismaMaster.evolution_api_config.findUnique({ + where: { id: instance.evolution_config_id }, + }); + if (!config) { + throw new BadRequestException('Configuração da Evolution API associada não encontrada'); + } + + await this.evolutionApiClient.enviarMensagem( + { + base_url: config.base_url, + api_key: this.encryptionService.decrypt(config.api_key_cripto), + }, + instance.instance_name, + numero, + mensagem, + ); + + return { sucesso: true }; + } catch (error) { + return { sucesso: false, erro: (error as Error).message }; + } + } + + async enviarEmail( + tenantId: string, + destinatario: string, + assunto: string, + corpo: string, + ): Promise { + try { + await this.emailService.enviar(tenantId, destinatario, assunto, corpo); + return { sucesso: true }; + } catch (error) { + return { sucesso: false, erro: (error as Error).message }; + } + } +} diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..68a6ce0 --- /dev/null +++ b/src/modules/auth/auth.controller.ts @@ -0,0 +1,119 @@ +import { Body, Controller, Get, HttpCode, HttpStatus, Post, Req, UseGuards } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; +import { Request } from 'express'; +import { PrismaMasterService } from '../../config/prisma-master.service'; +import { TenantContextService } from '../../config/tenant-context.service'; +import { AuthService, TokenPair } from './auth.service'; +import { CurrentUser } from './decorators/current-user.decorator'; +import { Roles } from './decorators/roles.decorator'; +import { ForgotPasswordDto } from './dto/forgot-password.dto'; +import { LoginDto } from './dto/login.dto'; +import { LogoutDto } from './dto/logout.dto'; +import { RefreshDto } from './dto/refresh.dto'; +import { ResetPasswordDto } from './dto/reset-password.dto'; +import { JwtAuthGuard } from './guards/jwt-auth.guard'; +import { MasterGuard } from './guards/master.guard'; +import { RolesGuard } from './guards/roles.guard'; +import { TenantGuard } from './guards/tenant.guard'; +import { JwtPayload } from './types/jwt-payload.type'; + +// Rate limit por IP nas rotas de login, como segunda camada além do +// bloqueio persistente por e-mail (`login_attempts`, ver +// AuthService.assertNotLockedOut — esse sim é o mecanismo de força bruta +// pedido no critério de aceite, com limite bem mais estrito). Este aqui é +// só uma rede de segurança contra um IP martelando várias contas +// diferentes; por isso o limite é generoso (não deve incomodar um IP +// legítimo, ex: escritório atrás de NAT com vários usuários). +const LOGIN_THROTTLE = { default: { limit: 30, ttl: 60_000 } }; + +function requestMeta(req: Request) { + return { ip: req.ip, userAgent: req.get('user-agent') }; +} + +@Controller('auth') +export class AuthController { + constructor( + private readonly authService: AuthService, + private readonly prismaMaster: PrismaMasterService, + private readonly tenantContext: TenantContextService, + ) {} + + @HttpCode(HttpStatus.OK) + @Throttle(LOGIN_THROTTLE) + @Post('master/login') + loginMaster(@Body() dto: LoginDto, @Req() req: Request): Promise { + return this.authService.loginMaster(dto, requestMeta(req)); + } + + @HttpCode(HttpStatus.OK) + @Throttle(LOGIN_THROTTLE) + @Post('tenant/login') + loginTenant(@Body() dto: LoginDto, @Req() req: Request): Promise { + return this.authService.loginTenant(dto, requestMeta(req)); + } + + @HttpCode(HttpStatus.OK) + @Post('refresh') + refresh(@Body() dto: RefreshDto, @Req() req: Request): Promise { + return this.authService.refresh(dto.refresh_token, requestMeta(req)); + } + + @HttpCode(HttpStatus.NO_CONTENT) + @Post('logout') + async logout(@Body() dto: LogoutDto): Promise { + await this.authService.logout(dto.refresh_token); + } + + @HttpCode(HttpStatus.OK) + @Post('password/forgot') + forgotPassword(@Body() dto: ForgotPasswordDto): Promise<{ message: string }> { + return this.authService.forgotPassword(dto); + } + + @HttpCode(HttpStatus.OK) + @Post('password/reset') + resetPassword(@Body() dto: ResetPasswordDto): Promise<{ message: string }> { + return this.authService.resetPassword(dto); + } + + /** Qualquer usuário autenticado (master ou tenant_user) — demonstra @CurrentUser()/JwtAuthGuard. */ + @UseGuards(JwtAuthGuard) + @Get('me') + me(@CurrentUser() user: JwtPayload): JwtPayload { + return user; + } + + /** Só master — demonstra MasterGuard + conexão BYPASSRLS (app_master). */ + @UseGuards(JwtAuthGuard, MasterGuard) + @Get('master/admins') + listMasterAdmins() { + return this.prismaMaster.master_admins.findMany({ + select: { + id: true, + nome: true, + email: true, + ativo: true, + ultimo_login: true, + criado_em: true, + }, + orderBy: { criado_em: 'asc' }, + }); + } + + /** + * Só tenant_admin — demonstra TenantGuard + RolesGuard + o mecanismo de + * RLS (TenantRlsInterceptor/TenantContextService, conexão app_tenant). + * `tenant_id` de query é ignorado de propósito: o isolamento vem sempre + * da sessão (token), nunca de input do usuário. + */ + @UseGuards(JwtAuthGuard, TenantGuard, RolesGuard) + @Roles('tenant_admin') + @Get('tenant/users') + listTenantUsers() { + const tx = this.tenantContext.getTx(); + return tx.tenant_users.findMany({ + select: { id: true, nome: true, email: true, role: true, ativo: true, criado_em: true }, + orderBy: { criado_em: 'asc' }, + }); + } +} diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts new file mode 100644 index 0000000..b1a5a7e --- /dev/null +++ b/src/modules/auth/auth.module.ts @@ -0,0 +1,38 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { APP_INTERCEPTOR } from '@nestjs/core'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { JwtAuthGuard } from './guards/jwt-auth.guard'; +import { MasterGuard } from './guards/master.guard'; +import { RolesGuard } from './guards/roles.guard'; +import { TenantGuard } from './guards/tenant.guard'; +import { TenantRlsInterceptor } from './interceptors/tenant-rls.interceptor'; +import { JwtStrategy } from './strategies/jwt.strategy'; + +@Module({ + imports: [ + PassportModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET'), + }), + }), + ], + controllers: [AuthController], + providers: [ + AuthService, + JwtStrategy, + JwtAuthGuard, + MasterGuard, + TenantGuard, + RolesGuard, + { provide: APP_INTERCEPTOR, useClass: TenantRlsInterceptor }, + ], + exports: [AuthService], +}) +export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..45047b1 --- /dev/null +++ b/src/modules/auth/auth.service.ts @@ -0,0 +1,321 @@ +import { HttpException, HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { user_role } from '@prisma/client'; +import { PrismaService } from '../../config/prisma.service'; +import { ForgotPasswordDto } from './dto/forgot-password.dto'; +import { LoginDto } from './dto/login.dto'; +import { ResetPasswordDto } from './dto/reset-password.dto'; +import { assertPasswordStrength, hashPassword, verifyPassword } from './password.util'; +import { expiresInToSeconds } from './time.util'; +import { generateOpaqueToken, hashToken } from './token.util'; +import { JwtPayload, UserType } from './types/jwt-payload.type'; + +export interface TokenPair { + access_token: string; + refresh_token: string; + token_type: 'Bearer'; + expires_in: number; +} + +export interface RequestMeta { + ip?: string; + userAgent?: string; +} + +/** Status de tenant que bloqueiam login/acesso dos seus `tenant_users` — reutilizado pelo `TenantGuard` para bloquear em toda request, não só no login. */ +export const TENANT_BLOCKED_STATUSES = new Set(['suspenso', 'cancelado']); + +@Injectable() +export class AuthService { + private readonly accessTokenExpiresIn: string; + private readonly refreshTokenExpiresInDays: number; + private readonly passwordResetExpiresInHours: number; + private readonly loginMaxAttempts: number; + private readonly loginLockoutWindowMinutes: number; + + constructor( + private readonly prisma: PrismaService, + private readonly jwtService: JwtService, + private readonly configService: ConfigService, + ) { + this.accessTokenExpiresIn = this.configService.get('JWT_ACCESS_EXPIRES_IN', '15m'); + this.refreshTokenExpiresInDays = this.configService.get( + 'REFRESH_TOKEN_EXPIRES_IN_DAYS', + 30, + ); + this.passwordResetExpiresInHours = this.configService.get( + 'PASSWORD_RESET_EXPIRES_IN_HOURS', + 1, + ); + this.loginMaxAttempts = this.configService.get('LOGIN_MAX_ATTEMPTS', 5); + this.loginLockoutWindowMinutes = this.configService.get( + 'LOGIN_LOCKOUT_WINDOW_MINUTES', + 15, + ); + } + + async loginMaster(dto: LoginDto, meta: RequestMeta): Promise { + await this.assertNotLockedOut(dto.email, 'master'); + + const admin = await this.prisma.master_admins.findUnique({ where: { email: dto.email } }); + const senhaOk = admin?.ativo ? await verifyPassword(admin.senha_hash, dto.senha) : false; + + await this.recordLoginAttempt(dto.email, 'master', senhaOk, meta.ip); + + if (!admin || !admin.ativo || !senhaOk) { + throw new UnauthorizedException('E-mail ou senha inválidos'); + } + + await this.prisma.master_admins.update({ + where: { id: admin.id }, + data: { ultimo_login: new Date() }, + }); + + return this.issueTokenPair({ tipo: 'master', usuario_id: admin.id, tenant_id: null }, meta); + } + + async loginTenant(dto: LoginDto, meta: RequestMeta): Promise { + await this.assertNotLockedOut(dto.email, 'tenant_user'); + + // `email` é único por tenant (UNIQUE(tenant_id, email)), não + // globalmente. Se o mesmo e-mail existir em mais de um tenant não há + // como resolver a ambiguidade com segurança apenas com email+senha — + // trata como credencial inválida em vez de arriscar logar na conta + // errada. + const usuario = await this.uniqueTenantUserByEmail(dto.email); + const senhaOk = usuario?.ativo ? await verifyPassword(usuario.senha_hash, dto.senha) : false; + + await this.recordLoginAttempt(dto.email, 'tenant_user', senhaOk, meta.ip); + + if (!usuario || !usuario.ativo || !senhaOk) { + throw new UnauthorizedException('E-mail ou senha inválidos'); + } + + const tenant = await this.prisma.tenants.findUnique({ where: { id: usuario.tenant_id } }); + if (!tenant || TENANT_BLOCKED_STATUSES.has(tenant.status)) { + throw new UnauthorizedException('Acesso bloqueado para este tenant'); + } + + await this.prisma.tenant_users.update({ + where: { id: usuario.id }, + data: { ultimo_login: new Date() }, + }); + + return this.issueTokenPair( + { + tipo: 'tenant_user', + usuario_id: usuario.id, + tenant_id: usuario.tenant_id, + role: usuario.role, + }, + meta, + ); + } + + async refresh(refreshToken: string, meta: RequestMeta): Promise { + const tokenHash = hashToken(refreshToken); + const stored = await this.prisma.auth_refresh_tokens.findFirst({ + where: { token_hash: tokenHash }, + }); + + if (!stored || stored.revogado || stored.expira_em < new Date()) { + if (stored?.revogado) { + // Reuso de um refresh token já rotacionado é indício de roubo de + // token: revoga todos os tokens ativos desse usuário por precaução. + await this.prisma.auth_refresh_tokens.updateMany({ + where: { + usuario_tipo: stored.usuario_tipo, + usuario_id: stored.usuario_id, + revogado: false, + }, + data: { revogado: true }, + }); + } + throw new UnauthorizedException('Refresh token inválido ou expirado'); + } + + await this.prisma.auth_refresh_tokens.update({ + where: { id: stored.id }, + data: { revogado: true }, + }); + + const tipo = stored.usuario_tipo as UserType; + + if (tipo === 'master') { + const admin = await this.prisma.master_admins.findUnique({ + where: { id: stored.usuario_id }, + }); + if (!admin || !admin.ativo) { + throw new UnauthorizedException('Usuário inválido'); + } + return this.issueTokenPair({ tipo: 'master', usuario_id: admin.id, tenant_id: null }, meta); + } + + const usuario = await this.prisma.tenant_users.findUnique({ where: { id: stored.usuario_id } }); + if (!usuario || !usuario.ativo) { + throw new UnauthorizedException('Usuário inválido'); + } + return this.issueTokenPair( + { + tipo: 'tenant_user', + usuario_id: usuario.id, + tenant_id: usuario.tenant_id, + role: usuario.role, + }, + meta, + ); + } + + async logout(refreshToken: string): Promise { + const tokenHash = hashToken(refreshToken); + await this.prisma.auth_refresh_tokens.updateMany({ + where: { token_hash: tokenHash, revogado: false }, + data: { revogado: true }, + }); + } + + async forgotPassword(dto: ForgotPasswordDto): Promise<{ message: string }> { + // Resposta genérica sempre igual (exista ou não a conta) para não + // revelar quais e-mails estão cadastrados. + const generic = { + message: 'Se o e-mail existir, as instruções de redefinição foram enviadas.', + }; + + const usuario = + dto.usuario_tipo === 'master' + ? await this.prisma.master_admins.findUnique({ where: { email: dto.email } }) + : await this.uniqueTenantUserByEmail(dto.email); + + if (!usuario || !usuario.ativo) { + return generic; + } + + const token = generateOpaqueToken(); + const expira_em = new Date(Date.now() + this.passwordResetExpiresInHours * 60 * 60 * 1000); + + await this.prisma.password_reset_tokens.create({ + data: { + usuario_tipo: dto.usuario_tipo, + usuario_id: usuario.id, + token_hash: hashToken(token), + expira_em, + }, + }); + + // Envio real de e-mail é de uma etapa futura (Etapa 4 — Integrações); + // por ora, só loga o token/link no console, como pedido no prompt desta etapa. + + console.log( + `[password-reset] usuario_tipo=${dto.usuario_tipo} usuario_id=${usuario.id} ` + + `token=${token} expira_em=${expira_em.toISOString()}`, + ); + + return generic; + } + + async resetPassword(dto: ResetPasswordDto): Promise<{ message: string }> { + assertPasswordStrength(dto.nova_senha); + + const tokenHash = hashToken(dto.token); + const registro = await this.prisma.password_reset_tokens.findFirst({ + where: { token_hash: tokenHash }, + }); + + if (!registro || registro.usado || registro.expira_em < new Date()) { + throw new UnauthorizedException('Token de redefinição inválido ou expirado'); + } + + const senha_hash = await hashPassword(dto.nova_senha); + + await this.prisma.$transaction(async (tx) => { + await tx.password_reset_tokens.update({ where: { id: registro.id }, data: { usado: true } }); + + if (registro.usuario_tipo === 'master') { + await tx.master_admins.update({ where: { id: registro.usuario_id }, data: { senha_hash } }); + } else { + await tx.tenant_users.update({ where: { id: registro.usuario_id }, data: { senha_hash } }); + } + + await tx.auth_refresh_tokens.updateMany({ + where: { + usuario_tipo: registro.usuario_tipo, + usuario_id: registro.usuario_id, + revogado: false, + }, + data: { revogado: true }, + }); + }); + + return { message: 'Senha redefinida com sucesso.' }; + } + + private async uniqueTenantUserByEmail(email: string) { + const candidatos = await this.prisma.tenant_users.findMany({ where: { email } }); + return candidatos.length === 1 ? candidatos[0] : undefined; + } + + private async issueTokenPair( + params: { tipo: UserType; usuario_id: string; tenant_id: string | null; role?: user_role }, + meta: RequestMeta, + ): Promise { + const payload: JwtPayload = { + sub: params.usuario_id, + tipo: params.tipo, + tenant_id: params.tenant_id, + ...(params.role ? { role: params.role } : {}), + }; + + const access_token = await this.jwtService.signAsync(payload, { + // `expiresIn` aceita string ("15m"/"1h"/...), mas o tipo `StringValue` + // da lib `ms` só cobre literais fixos — aqui o valor vem de env/config. + expiresIn: this.accessTokenExpiresIn as unknown as number, + }); + + const refresh_token = generateOpaqueToken(); + const expira_em = new Date(Date.now() + this.refreshTokenExpiresInDays * 24 * 60 * 60 * 1000); + + await this.prisma.auth_refresh_tokens.create({ + data: { + usuario_tipo: params.tipo, + usuario_id: params.usuario_id, + token_hash: hashToken(refresh_token), + expira_em, + ip_origem: meta.ip, + user_agent: meta.userAgent, + }, + }); + + return { + access_token, + refresh_token, + token_type: 'Bearer', + expires_in: expiresInToSeconds(this.accessTokenExpiresIn), + }; + } + + private async assertNotLockedOut(email: string, usuario_tipo: UserType): Promise { + const desde = new Date(Date.now() - this.loginLockoutWindowMinutes * 60 * 1000); + const falhas = await this.prisma.login_attempts.count({ + where: { email, usuario_tipo, sucesso: false, criado_em: { gte: desde } }, + }); + + if (falhas >= this.loginMaxAttempts) { + throw new HttpException( + `Muitas tentativas de login falhas para este e-mail. Tente novamente em até ${this.loginLockoutWindowMinutes} minutos.`, + HttpStatus.TOO_MANY_REQUESTS, + ); + } + } + + private async recordLoginAttempt( + email: string, + usuario_tipo: UserType, + sucesso: boolean, + ip?: string, + ): Promise { + await this.prisma.login_attempts.create({ + data: { email, usuario_tipo, sucesso, ip_origem: ip }, + }); + } +} diff --git a/src/modules/auth/decorators/current-user.decorator.ts b/src/modules/auth/decorators/current-user.decorator.ts new file mode 100644 index 0000000..d779d57 --- /dev/null +++ b/src/modules/auth/decorators/current-user.decorator.ts @@ -0,0 +1,7 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { AuthenticatedRequest } from '../types/authenticated-request.type'; + +export const CurrentUser = createParamDecorator((_data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return request.user; +}); diff --git a/src/modules/auth/decorators/roles.decorator.ts b/src/modules/auth/decorators/roles.decorator.ts new file mode 100644 index 0000000..d0c74db --- /dev/null +++ b/src/modules/auth/decorators/roles.decorator.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from '@nestjs/common'; +import { user_role } from '@prisma/client'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: user_role[]) => SetMetadata(ROLES_KEY, roles); diff --git a/src/modules/auth/dto/forgot-password.dto.ts b/src/modules/auth/dto/forgot-password.dto.ts new file mode 100644 index 0000000..b9ae0ba --- /dev/null +++ b/src/modules/auth/dto/forgot-password.dto.ts @@ -0,0 +1,10 @@ +import { IsEmail, IsIn } from 'class-validator'; +import { UserType } from '../types/jwt-payload.type'; + +export class ForgotPasswordDto { + @IsEmail() + email: string; + + @IsIn(['master', 'tenant_user']) + usuario_tipo: UserType; +} diff --git a/src/modules/auth/dto/login.dto.ts b/src/modules/auth/dto/login.dto.ts new file mode 100644 index 0000000..1837224 --- /dev/null +++ b/src/modules/auth/dto/login.dto.ts @@ -0,0 +1,10 @@ +import { IsEmail, IsString, MinLength } from 'class-validator'; + +export class LoginDto { + @IsEmail() + email: string; + + @IsString() + @MinLength(1) + senha: string; +} diff --git a/src/modules/auth/dto/logout.dto.ts b/src/modules/auth/dto/logout.dto.ts new file mode 100644 index 0000000..67120b3 --- /dev/null +++ b/src/modules/auth/dto/logout.dto.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator'; + +export class LogoutDto { + @IsString() + refresh_token: string; +} diff --git a/src/modules/auth/dto/refresh.dto.ts b/src/modules/auth/dto/refresh.dto.ts new file mode 100644 index 0000000..db04f27 --- /dev/null +++ b/src/modules/auth/dto/refresh.dto.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator'; + +export class RefreshDto { + @IsString() + refresh_token: string; +} diff --git a/src/modules/auth/dto/reset-password.dto.ts b/src/modules/auth/dto/reset-password.dto.ts new file mode 100644 index 0000000..3fd1724 --- /dev/null +++ b/src/modules/auth/dto/reset-password.dto.ts @@ -0,0 +1,10 @@ +import { IsString, MinLength } from 'class-validator'; + +export class ResetPasswordDto { + @IsString() + token: string; + + @IsString() + @MinLength(10) + nova_senha: string; +} diff --git a/src/modules/auth/guards/jwt-auth.guard.ts b/src/modules/auth/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..2155290 --- /dev/null +++ b/src/modules/auth/guards/jwt-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') {} diff --git a/src/modules/auth/guards/master.guard.ts b/src/modules/auth/guards/master.guard.ts new file mode 100644 index 0000000..86b0fc7 --- /dev/null +++ b/src/modules/auth/guards/master.guard.ts @@ -0,0 +1,16 @@ +import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common'; +import { AuthenticatedRequest } from '../types/authenticated-request.type'; + +/** Permite apenas requests autenticadas como `master` (deve rodar depois de JwtAuthGuard). */ +@Injectable() +export class MasterGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + + if (request.user?.tipo !== 'master') { + throw new ForbiddenException('Rota restrita ao painel master'); + } + + return true; + } +} diff --git a/src/modules/auth/guards/roles.guard.ts b/src/modules/auth/guards/roles.guard.ts new file mode 100644 index 0000000..11dec00 --- /dev/null +++ b/src/modules/auth/guards/roles.guard.ts @@ -0,0 +1,31 @@ +import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { user_role } from '@prisma/client'; +import { ROLES_KEY } from '../decorators/roles.decorator'; +import { AuthenticatedRequest } from '../types/authenticated-request.type'; + +/** Restringe por papel dentro do tenant (`@Roles('tenant_admin', ...)`). Sem decorator, libera. */ +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private readonly reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + if (!requiredRoles || requiredRoles.length === 0) { + return true; + } + + const request = context.switchToHttp().getRequest(); + const role = request.user?.role; + + if (!role || !requiredRoles.includes(role)) { + throw new ForbiddenException('Papel do usuário não tem permissão para esta rota'); + } + + return true; + } +} diff --git a/src/modules/auth/guards/tenant.guard.ts b/src/modules/auth/guards/tenant.guard.ts new file mode 100644 index 0000000..ba784ef --- /dev/null +++ b/src/modules/auth/guards/tenant.guard.ts @@ -0,0 +1,39 @@ +import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common'; +import { PrismaMasterService } from '../../../config/prisma-master.service'; +import { TENANT_BLOCKED_STATUSES } from '../auth.service'; +import { AuthenticatedRequest } from '../types/authenticated-request.type'; + +/** + * Permite apenas requests autenticadas como `tenant_user` (deve rodar + * depois de JwtAuthGuard), bloqueia se o tenant estiver `suspenso`/ + * `cancelado` (checado a cada request, não só no login — um access token + * emitido antes da suspensão continua válido até expirar, então o corte de + * acesso não pode depender só da checagem feita em `AuthService.loginTenant`) + * e expõe `tenant_id` no contexto da request — o `TenantRlsInterceptor` usa + * esse mesmo `request.user.tenant_id` (nunca um valor vindo de body/query) + * para abrir a transação com RLS. + */ +@Injectable() +export class TenantGuard implements CanActivate { + constructor(private readonly prismaMaster: PrismaMasterService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + + if (request.user?.tipo !== 'tenant_user' || !request.user.tenant_id) { + throw new ForbiddenException('Rota restrita a usuários de tenant'); + } + + const tenant = await this.prismaMaster.tenants.findUnique({ + where: { id: request.user.tenant_id }, + select: { status: true }, + }); + + if (!tenant || TENANT_BLOCKED_STATUSES.has(tenant.status)) { + throw new ForbiddenException('Acesso bloqueado para este tenant'); + } + + request.tenant_id = request.user.tenant_id; + return true; + } +} diff --git a/src/modules/auth/interceptors/tenant-rls.interceptor.ts b/src/modules/auth/interceptors/tenant-rls.interceptor.ts new file mode 100644 index 0000000..6da4def --- /dev/null +++ b/src/modules/auth/interceptors/tenant-rls.interceptor.ts @@ -0,0 +1,32 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import { from, Observable, firstValueFrom } from 'rxjs'; +import { TenantContextService } from '../../../config/tenant-context.service'; +import { AuthenticatedRequest } from '../types/authenticated-request.type'; + +/** + * Roda em toda request HTTP (registrado globalmente). Para requests + * autenticadas como `tenant_user`, envolve o handler numa transação Prisma + * na conexão `app_tenant` com `SET LOCAL app.current_tenant_id` (via + * `TenantContextService`), usando **sempre** o `tenant_id` do token + * (`request.user`, populado pelo JwtStrategy/guards) — nunca um valor de + * input do usuário. Requests master ou não autenticadas passam direto. + */ +@Injectable() +export class TenantRlsInterceptor implements NestInterceptor { + constructor(private readonly tenantContext: TenantContextService) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + if (context.getType() !== 'http') { + return next.handle(); + } + + const request = context.switchToHttp().getRequest(); + const user = request.user; + + if (!user || user.tipo !== 'tenant_user' || !user.tenant_id) { + return next.handle(); + } + + return from(this.tenantContext.run(user.tenant_id, () => firstValueFrom(next.handle()))); + } +} diff --git a/src/modules/auth/password.util.ts b/src/modules/auth/password.util.ts new file mode 100644 index 0000000..61c28cc --- /dev/null +++ b/src/modules/auth/password.util.ts @@ -0,0 +1,47 @@ +import { BadRequestException } from '@nestjs/common'; +import * as argon2 from 'argon2'; + +// Parâmetros OWASP (2024) para argon2id em contexto de login web: 19 MiB de +// memória, 2 iterações, paralelismo 1. +const ARGON2_OPTIONS: argon2.HashOptions = { + type: argon2.argon2id, + memoryCost: 19456, + timeCost: 2, + parallelism: 1, +}; + +export function hashPassword(password: string): Promise { + return argon2.hash(password, ARGON2_OPTIONS); +} + +export function verifyPassword(hash: string, password: string): Promise { + return argon2.verify(hash, password); +} + +const MIN_LENGTH = 10; + +const TRIVIAL_PASSWORDS = new Set([ + '12345678', + '123456789', + '1234567890', + 'password', + 'password1', + 'password123', + 'senha1234', + 'senha12345', + 'senha123456', + 'qwertyuiop', + '11111111', + '00000000', + 'admin1234', + 'letmein123', +]); + +export function assertPasswordStrength(password: string): void { + if (password.length < MIN_LENGTH) { + throw new BadRequestException(`A senha deve ter no mínimo ${MIN_LENGTH} caracteres`); + } + if (TRIVIAL_PASSWORDS.has(password.toLowerCase())) { + throw new BadRequestException('Senha muito comum/trivial, escolha outra'); + } +} diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..c11e6ae --- /dev/null +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { JwtPayload } from '../types/jwt-payload.type'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor(configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.getOrThrow('JWT_SECRET'), + }); + } + + validate(payload: JwtPayload): JwtPayload { + return payload; + } +} diff --git a/src/modules/auth/time.util.ts b/src/modules/auth/time.util.ts new file mode 100644 index 0000000..099d782 --- /dev/null +++ b/src/modules/auth/time.util.ts @@ -0,0 +1,10 @@ +const UNIT_SECONDS: Record = { s: 1, m: 60, h: 3600, d: 86400 }; + +/** Converte strings simples do tipo "15m"/"1h"/"30d" (mesmo formato aceito por JwtModule) em segundos. */ +export function expiresInToSeconds(value: string): number { + const match = /^(\d+)(s|m|h|d)$/.exec(value.trim()); + if (!match) { + throw new Error(`Formato de expiração inválido: "${value}" (use algo como "15m", "1h", "30d")`); + } + return Number(match[1]) * UNIT_SECONDS[match[2]]; +} diff --git a/src/modules/auth/token.util.ts b/src/modules/auth/token.util.ts new file mode 100644 index 0000000..e9ae084 --- /dev/null +++ b/src/modules/auth/token.util.ts @@ -0,0 +1,9 @@ +import { createHash, randomBytes } from 'node:crypto'; + +export function generateOpaqueToken(): string { + return randomBytes(48).toString('base64url'); +} + +export function hashToken(token: string): string { + return createHash('sha256').update(token).digest('hex'); +} diff --git a/src/modules/auth/types/authenticated-request.type.ts b/src/modules/auth/types/authenticated-request.type.ts new file mode 100644 index 0000000..366c8ed --- /dev/null +++ b/src/modules/auth/types/authenticated-request.type.ts @@ -0,0 +1,8 @@ +import { Request } from 'express'; +import { JwtPayload } from './jwt-payload.type'; + +export interface AuthenticatedRequest extends Request { + user: JwtPayload; + /** Preenchido pelo TenantGuard a partir de `user.tenant_id` — nunca de input do usuário. */ + tenant_id?: string; +} diff --git a/src/modules/auth/types/jwt-payload.type.ts b/src/modules/auth/types/jwt-payload.type.ts new file mode 100644 index 0000000..c95fdd7 --- /dev/null +++ b/src/modules/auth/types/jwt-payload.type.ts @@ -0,0 +1,10 @@ +import { user_role } from '@prisma/client'; + +export type UserType = 'master' | 'tenant_user'; + +export interface JwtPayload { + sub: string; + tipo: UserType; + tenant_id: string | null; + role?: user_role; +} diff --git a/src/modules/email-config/dto/create-master-email-config.dto.ts b/src/modules/email-config/dto/create-master-email-config.dto.ts new file mode 100644 index 0000000..7a60fd5 --- /dev/null +++ b/src/modules/email-config/dto/create-master-email-config.dto.ts @@ -0,0 +1,24 @@ +import { IsEmail, IsInt, IsString, Max, Min, MinLength } from 'class-validator'; + +export class CreateMasterEmailConfigDto { + @IsString() + @MinLength(2) + smtp_host: string; + + @IsInt() + @Min(1) + @Max(65535) + smtp_porta: number; + + @IsString() + @MinLength(1) + smtp_usuario: string; + + /** Texto puro na entrada — cifrado (AES-256-GCM) antes de gravar em `smtp_senha_cripto`. */ + @IsString() + @MinLength(1) + smtp_senha: string; + + @IsEmail() + email_remetente: string; +} diff --git a/src/modules/email-config/dto/testar-envio-email.dto.ts b/src/modules/email-config/dto/testar-envio-email.dto.ts new file mode 100644 index 0000000..a98f9e2 --- /dev/null +++ b/src/modules/email-config/dto/testar-envio-email.dto.ts @@ -0,0 +1,6 @@ +import { IsEmail } from 'class-validator'; + +export class TestarEnvioEmailDto { + @IsEmail() + destinatario: string; +} diff --git a/src/modules/email-config/dto/update-master-email-config.dto.ts b/src/modules/email-config/dto/update-master-email-config.dto.ts new file mode 100644 index 0000000..95c711b --- /dev/null +++ b/src/modules/email-config/dto/update-master-email-config.dto.ts @@ -0,0 +1,42 @@ +import { + IsBoolean, + IsEmail, + IsInt, + IsOptional, + IsString, + Max, + Min, + MinLength, +} from 'class-validator'; + +export class UpdateMasterEmailConfigDto { + @IsOptional() + @IsString() + @MinLength(2) + smtp_host?: string; + + @IsOptional() + @IsInt() + @Min(1) + @Max(65535) + smtp_porta?: number; + + @IsOptional() + @IsString() + @MinLength(1) + smtp_usuario?: string; + + /** Texto puro na entrada — cifrado (AES-256-GCM) antes de gravar em `smtp_senha_cripto`. */ + @IsOptional() + @IsString() + @MinLength(1) + smtp_senha?: string; + + @IsOptional() + @IsEmail() + email_remetente?: string; + + @IsOptional() + @IsBoolean() + ativo?: boolean; +} diff --git a/src/modules/email-config/dto/update-tenant-email-config.dto.ts b/src/modules/email-config/dto/update-tenant-email-config.dto.ts new file mode 100644 index 0000000..558e1e4 --- /dev/null +++ b/src/modules/email-config/dto/update-tenant-email-config.dto.ts @@ -0,0 +1,47 @@ +import { + IsBoolean, + IsEmail, + IsInt, + IsString, + Max, + Min, + MinLength, + ValidateIf, +} from 'class-validator'; + +/** + * `usar_config_padrao=true`: ignora os campos de SMTP próprio (mesmo que + * enviados, o service não os persiste). `usar_config_padrao=false`: exige + * todos os campos de SMTP preenchidos (validado declarativamente via + * `@ValidateIf`). + */ +export class UpdateTenantEmailConfigDto { + @IsBoolean() + usar_config_padrao: boolean; + + @ValidateIf((o: UpdateTenantEmailConfigDto) => o.usar_config_padrao === false) + @IsString() + @MinLength(2) + smtp_host?: string; + + @ValidateIf((o: UpdateTenantEmailConfigDto) => o.usar_config_padrao === false) + @IsInt() + @Min(1) + @Max(65535) + smtp_porta?: number; + + @ValidateIf((o: UpdateTenantEmailConfigDto) => o.usar_config_padrao === false) + @IsString() + @MinLength(1) + smtp_usuario?: string; + + /** Texto puro na entrada — cifrado (AES-256-GCM) antes de gravar em `smtp_senha_cripto`. */ + @ValidateIf((o: UpdateTenantEmailConfigDto) => o.usar_config_padrao === false) + @IsString() + @MinLength(1) + smtp_senha?: string; + + @ValidateIf((o: UpdateTenantEmailConfigDto) => o.usar_config_padrao === false) + @IsEmail() + email_remetente?: string; +} diff --git a/src/modules/email-config/email-config.module.ts b/src/modules/email-config/email-config.module.ts new file mode 100644 index 0000000..10d7cb6 --- /dev/null +++ b/src/modules/email-config/email-config.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { AlertDispatchModule } from '../alert-dispatch/alert-dispatch.module'; +import { MasterEmailConfigController } from './master-email-config.controller'; +import { MasterEmailConfigService } from './master-email-config.service'; +import { TenantEmailConfigController } from './tenant-email-config.controller'; +import { TenantEmailConfigService } from './tenant-email-config.service'; + +@Module({ + imports: [AlertDispatchModule], + controllers: [MasterEmailConfigController, TenantEmailConfigController], + providers: [MasterEmailConfigService, TenantEmailConfigService], +}) +export class EmailConfigModule {} diff --git a/src/modules/email-config/email.module.ts b/src/modules/email-config/email.module.ts new file mode 100644 index 0000000..fff2146 --- /dev/null +++ b/src/modules/email-config/email.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { EmailService } from './email.service'; + +@Module({ + providers: [EmailService], + exports: [EmailService], +}) +export class EmailModule {} diff --git a/src/modules/email-config/email.service.ts b/src/modules/email-config/email.service.ts new file mode 100644 index 0000000..238b414 --- /dev/null +++ b/src/modules/email-config/email.service.ts @@ -0,0 +1,100 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { createTransport } from 'nodemailer'; +import { EncryptionService } from '../../common/encryption.service'; +import { PrismaMasterService } from '../../config/prisma-master.service'; + +interface ResolvedEmailConfig { + smtp_host: string; + smtp_porta: number; + smtp_usuario: string; + smtp_senha: string; + email_remetente: string; +} + +/** + * Resolve automaticamente qual SMTP usar para um tenant (próprio ou o + * padrão da plataforma) e envia via `nodemailer`. Fica num módulo próprio + * (`EmailModule`), separado do CRUD de `email-config`, porque o + * `AlertDispatchService` (Etapa 7) depende deste service e o CRUD de + * `email-config` depende do `AlertDispatchService` (endpoints de + * "testar envio") — um único módulo combinando os dois criaria um ciclo de + * imports entre `EmailConfigModule` e `AlertDispatchModule`. + */ +@Injectable() +export class EmailService { + constructor( + private readonly prismaMaster: PrismaMasterService, + private readonly encryptionService: EncryptionService, + ) {} + + async resolverConfig(tenantId: string): Promise { + const tenantConfig = await this.prismaMaster.tenant_email_config.findUnique({ + where: { tenant_id: tenantId }, + }); + + if (tenantConfig && !tenantConfig.usar_config_padrao) { + if ( + !tenantConfig.smtp_host || + !tenantConfig.smtp_porta || + !tenantConfig.smtp_usuario || + !tenantConfig.smtp_senha_cripto || + !tenantConfig.email_remetente + ) { + throw new BadRequestException('Configuração de SMTP própria do tenant está incompleta'); + } + + return { + smtp_host: tenantConfig.smtp_host, + smtp_porta: tenantConfig.smtp_porta, + smtp_usuario: tenantConfig.smtp_usuario, + smtp_senha: this.encryptionService.decrypt(tenantConfig.smtp_senha_cripto), + email_remetente: tenantConfig.email_remetente, + }; + } + + const padrao = await this.prismaMaster.master_email_config.findFirst({ + where: { ativo: true }, + orderBy: { criado_em: 'desc' }, + }); + if (!padrao) { + throw new BadRequestException( + 'Nenhuma configuração de e-mail padrão da plataforma está ativa', + ); + } + + return { + smtp_host: padrao.smtp_host, + smtp_porta: padrao.smtp_porta, + smtp_usuario: padrao.smtp_usuario, + smtp_senha: this.encryptionService.decrypt(padrao.smtp_senha_cripto), + email_remetente: padrao.email_remetente, + }; + } + + async enviar( + tenantId: string, + destinatario: string, + assunto: string, + corpo: string, + ): Promise { + const config = await this.resolverConfig(tenantId); + + const transporter = createTransport({ + host: config.smtp_host, + port: config.smtp_porta, + secure: config.smtp_porta === 465, + auth: { user: config.smtp_usuario, pass: config.smtp_senha }, + }); + + try { + await transporter.sendMail({ + from: config.email_remetente, + to: destinatario, + subject: assunto, + html: corpo, + }); + } catch (error) { + throw new BadRequestException(`Falha ao enviar e-mail: ${(error as Error).message}`); + } + } +} diff --git a/src/modules/email-config/master-email-config.controller.ts b/src/modules/email-config/master-email-config.controller.ts new file mode 100644 index 0000000..2b2250b --- /dev/null +++ b/src/modules/email-config/master-email-config.controller.ts @@ -0,0 +1,67 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + ParseUUIDPipe, + Post, + Put, + Req, + UseGuards, +} from '@nestjs/common'; +import { Request } from 'express'; +import { CurrentUser } from '../auth/decorators/current-user.decorator'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { MasterGuard } from '../auth/guards/master.guard'; +import { JwtPayload } from '../auth/types/jwt-payload.type'; +import { CreateMasterEmailConfigDto } from './dto/create-master-email-config.dto'; +import { UpdateMasterEmailConfigDto } from './dto/update-master-email-config.dto'; +import { MasterEmailConfigService } from './master-email-config.service'; + +@UseGuards(JwtAuthGuard, MasterGuard) +@Controller('master/email-config') +export class MasterEmailConfigController { + constructor(private readonly masterEmailConfigService: MasterEmailConfigService) {} + + @Post() + create( + @Body() dto: CreateMasterEmailConfigDto, + @CurrentUser() user: JwtPayload, + @Req() req: Request, + ) { + return this.masterEmailConfigService.create(dto, user, req.ip); + } + + @Get() + findAll() { + return this.masterEmailConfigService.findAll(); + } + + @Get(':id') + findOne(@Param('id', ParseUUIDPipe) id: string) { + return this.masterEmailConfigService.findOne(id); + } + + @Put(':id') + update( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateMasterEmailConfigDto, + @CurrentUser() user: JwtPayload, + @Req() req: Request, + ) { + return this.masterEmailConfigService.update(id, dto, user, req.ip); + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + remove( + @Param('id', ParseUUIDPipe) id: string, + @CurrentUser() user: JwtPayload, + @Req() req: Request, + ) { + return this.masterEmailConfigService.remove(id, user, req.ip); + } +} diff --git a/src/modules/email-config/master-email-config.service.ts b/src/modules/email-config/master-email-config.service.ts new file mode 100644 index 0000000..432a007 --- /dev/null +++ b/src/modules/email-config/master-email-config.service.ts @@ -0,0 +1,120 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { master_email_config } from '@prisma/client'; +import { writeAuditLog } from '../../common/audit-log.util'; +import { EncryptionService } from '../../common/encryption.service'; +import { maskSecret } from '../../common/mask.util'; +import { PrismaMasterService } from '../../config/prisma-master.service'; +import { JwtPayload } from '../auth/types/jwt-payload.type'; +import { CreateMasterEmailConfigDto } from './dto/create-master-email-config.dto'; +import { UpdateMasterEmailConfigDto } from './dto/update-master-email-config.dto'; + +@Injectable() +export class MasterEmailConfigService { + constructor( + private readonly prismaMaster: PrismaMasterService, + private readonly encryptionService: EncryptionService, + ) {} + + /** Nunca retorna `smtp_senha_cripto` — só uma versão mascarada (últimos 4 caracteres). */ + private sanitize(config: master_email_config) { + const { smtp_senha_cripto, ...rest } = config; + return { ...rest, smtp_senha: maskSecret(this.encryptionService.decrypt(smtp_senha_cripto)) }; + } + + private async findRaw(id: string): Promise { + const config = await this.prismaMaster.master_email_config.findUnique({ where: { id } }); + if (!config) { + throw new NotFoundException('Configuração de e-mail padrão não encontrada'); + } + return config; + } + + async create(dto: CreateMasterEmailConfigDto, actor: JwtPayload, ip?: string) { + return this.prismaMaster.$transaction(async (tx) => { + const config = await tx.master_email_config.create({ + data: { + smtp_host: dto.smtp_host, + smtp_porta: dto.smtp_porta, + smtp_usuario: dto.smtp_usuario, + smtp_senha_cripto: this.encryptionService.encrypt(dto.smtp_senha), + email_remetente: dto.email_remetente, + }, + }); + + await writeAuditLog(tx, { + usuario_tipo: 'master', + usuario_id: actor.sub, + acao: 'criar_master_email_config', + entidade: 'master_email_config', + entidade_id: config.id, + detalhes: { smtp_host: dto.smtp_host, email_remetente: dto.email_remetente }, + ip_origem: ip, + }); + + return this.sanitize(config); + }); + } + + async findAll() { + const configs = await this.prismaMaster.master_email_config.findMany({ + orderBy: { criado_em: 'asc' }, + }); + return configs.map((config) => this.sanitize(config)); + } + + async findOne(id: string) { + return this.sanitize(await this.findRaw(id)); + } + + async update(id: string, dto: UpdateMasterEmailConfigDto, actor: JwtPayload, ip?: string) { + await this.findRaw(id); + + return this.prismaMaster.$transaction(async (tx) => { + const config = await tx.master_email_config.update({ + where: { id }, + data: { + ...(dto.smtp_host !== undefined ? { smtp_host: dto.smtp_host } : {}), + ...(dto.smtp_porta !== undefined ? { smtp_porta: dto.smtp_porta } : {}), + ...(dto.smtp_usuario !== undefined ? { smtp_usuario: dto.smtp_usuario } : {}), + ...(dto.smtp_senha !== undefined + ? { smtp_senha_cripto: this.encryptionService.encrypt(dto.smtp_senha) } + : {}), + ...(dto.email_remetente !== undefined ? { email_remetente: dto.email_remetente } : {}), + ...(dto.ativo !== undefined ? { ativo: dto.ativo } : {}), + }, + }); + + await writeAuditLog(tx, { + usuario_tipo: 'master', + usuario_id: actor.sub, + acao: 'atualizar_master_email_config', + entidade: 'master_email_config', + entidade_id: id, + detalhes: { + ...dto, + smtp_senha: dto.smtp_senha !== undefined ? '***alterada***' : undefined, + }, + ip_origem: ip, + }); + + return this.sanitize(config); + }); + } + + async remove(id: string, actor: JwtPayload, ip?: string) { + await this.findRaw(id); + + await this.prismaMaster.$transaction(async (tx) => { + await tx.master_email_config.delete({ where: { id } }); + + await writeAuditLog(tx, { + usuario_tipo: 'master', + usuario_id: actor.sub, + acao: 'excluir_master_email_config', + entidade: 'master_email_config', + entidade_id: id, + ip_origem: ip, + }); + }); + } +} diff --git a/src/modules/email-config/tenant-email-config.controller.ts b/src/modules/email-config/tenant-email-config.controller.ts new file mode 100644 index 0000000..a111222 --- /dev/null +++ b/src/modules/email-config/tenant-email-config.controller.ts @@ -0,0 +1,37 @@ +import { Body, Controller, Get, Post, Put, Req, UseGuards } from '@nestjs/common'; +import { Request } from 'express'; +import { CurrentUser } from '../auth/decorators/current-user.decorator'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { Roles } from '../auth/decorators/roles.decorator'; +import { RolesGuard } from '../auth/guards/roles.guard'; +import { TenantGuard } from '../auth/guards/tenant.guard'; +import { JwtPayload } from '../auth/types/jwt-payload.type'; +import { TestarEnvioEmailDto } from './dto/testar-envio-email.dto'; +import { UpdateTenantEmailConfigDto } from './dto/update-tenant-email-config.dto'; +import { TenantEmailConfigService } from './tenant-email-config.service'; + +@UseGuards(JwtAuthGuard, TenantGuard, RolesGuard) +@Roles('tenant_admin') +@Controller('tenant/email-config') +export class TenantEmailConfigController { + constructor(private readonly tenantEmailConfigService: TenantEmailConfigService) {} + + @Get() + obter() { + return this.tenantEmailConfigService.obter(); + } + + @Put() + atualizar( + @Body() dto: UpdateTenantEmailConfigDto, + @CurrentUser() user: JwtPayload, + @Req() req: Request, + ) { + return this.tenantEmailConfigService.atualizar(dto, user, req.ip); + } + + @Post('testar-envio') + testarEnvio(@Body() dto: TestarEnvioEmailDto, @CurrentUser() user: JwtPayload) { + return this.tenantEmailConfigService.testarEnvio(user, dto); + } +} diff --git a/src/modules/email-config/tenant-email-config.service.ts b/src/modules/email-config/tenant-email-config.service.ts new file mode 100644 index 0000000..b917dc0 --- /dev/null +++ b/src/modules/email-config/tenant-email-config.service.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@nestjs/common'; +import { EncryptionService } from '../../common/encryption.service'; +import { maskSecret } from '../../common/mask.util'; +import { TenantContextService } from '../../config/tenant-context.service'; +import { AlertDispatchService } from '../alert-dispatch/alert-dispatch.service'; +import { writeAuditLog } from '../../common/audit-log.util'; +import { JwtPayload } from '../auth/types/jwt-payload.type'; +import { TestarEnvioEmailDto } from './dto/testar-envio-email.dto'; +import { UpdateTenantEmailConfigDto } from './dto/update-tenant-email-config.dto'; + +const ASSUNTO_TESTE = 'Teste de configuração de e-mail — SeConectai Vendas'; +const CORPO_TESTE = + 'Este é um e-mail de teste enviado pelo SeConectai Vendas para validar sua configuração de SMTP.'; + +@Injectable() +export class TenantEmailConfigService { + constructor( + private readonly tenantContext: TenantContextService, + private readonly encryptionService: EncryptionService, + private readonly alertDispatchService: AlertDispatchService, + ) {} + + /** Quando usando o padrão da plataforma, oculta os campos de SMTP próprio (podem estar desatualizados). */ + async obter() { + const tx = this.tenantContext.getTx(); + const config = await tx.tenant_email_config.findUnique({ + where: { tenant_id: this.tenantContext.getTenantId() }, + }); + + if (!config || config.usar_config_padrao) { + return { usar_config_padrao: true }; + } + + return { + usar_config_padrao: false, + smtp_host: config.smtp_host, + smtp_porta: config.smtp_porta, + smtp_usuario: config.smtp_usuario, + smtp_senha: config.smtp_senha_cripto + ? maskSecret(this.encryptionService.decrypt(config.smtp_senha_cripto)) + : undefined, + email_remetente: config.email_remetente, + }; + } + + async atualizar(dto: UpdateTenantEmailConfigDto, actor: JwtPayload, ip?: string) { + const tenantId = actor.tenant_id as string; + const tx = this.tenantContext.getTx(); + + const campos = dto.usar_config_padrao + ? { usar_config_padrao: true as const } + : { + usar_config_padrao: false as const, + smtp_host: dto.smtp_host as string, + smtp_porta: dto.smtp_porta as number, + smtp_usuario: dto.smtp_usuario as string, + smtp_senha_cripto: this.encryptionService.encrypt(dto.smtp_senha as string), + email_remetente: dto.email_remetente as string, + }; + + const config = await tx.tenant_email_config.upsert({ + where: { tenant_id: tenantId }, + create: { tenant_id: tenantId, ...campos }, + update: campos, + }); + + await writeAuditLog(tx, { + usuario_tipo: 'tenant_user', + usuario_id: actor.sub, + tenant_id: tenantId, + acao: 'atualizar_tenant_email_config', + entidade: 'tenant_email_config', + entidade_id: config.id, + detalhes: { usar_config_padrao: dto.usar_config_padrao }, + ip_origem: ip, + }); + + return this.obter(); + } + + testarEnvio(actor: JwtPayload, dto: TestarEnvioEmailDto) { + const tenantId = actor.tenant_id as string; + return this.alertDispatchService.enviarEmail( + tenantId, + dto.destinatario, + ASSUNTO_TESTE, + CORPO_TESTE, + ); + } +} diff --git a/src/modules/evolution/dto/create-evolution-config.dto.ts b/src/modules/evolution/dto/create-evolution-config.dto.ts new file mode 100644 index 0000000..66c91d1 --- /dev/null +++ b/src/modules/evolution/dto/create-evolution-config.dto.ts @@ -0,0 +1,20 @@ +import { IsOptional, IsString, IsUrl, MinLength } from 'class-validator'; + +export class CreateEvolutionConfigDto { + @IsString() + @MinLength(2) + nome: string; + + @IsUrl({ protocols: ['http', 'https'], require_tld: false, require_protocol: true }) + base_url: string; + + /** Texto puro na entrada — cifrado (AES-256-GCM) antes de gravar em `api_key_cripto`. */ + @IsString() + @MinLength(8) + api_key: string; + + @IsOptional() + @IsString() + @MinLength(1) + versao?: string; +} diff --git a/src/modules/evolution/dto/update-evolution-config.dto.ts b/src/modules/evolution/dto/update-evolution-config.dto.ts new file mode 100644 index 0000000..6d83f75 --- /dev/null +++ b/src/modules/evolution/dto/update-evolution-config.dto.ts @@ -0,0 +1,27 @@ +import { IsBoolean, IsOptional, IsString, IsUrl, MinLength } from 'class-validator'; + +export class UpdateEvolutionConfigDto { + @IsOptional() + @IsString() + @MinLength(2) + nome?: string; + + @IsOptional() + @IsUrl({ protocols: ['http', 'https'], require_tld: false, require_protocol: true }) + base_url?: string; + + /** Texto puro na entrada — cifrado (AES-256-GCM) antes de gravar em `api_key_cripto`. */ + @IsOptional() + @IsString() + @MinLength(8) + api_key?: string; + + @IsOptional() + @IsString() + @MinLength(1) + versao?: string; + + @IsOptional() + @IsBoolean() + ativo?: boolean; +} diff --git a/src/modules/evolution/evolution-api.client.ts b/src/modules/evolution/evolution-api.client.ts new file mode 100644 index 0000000..cb93e64 --- /dev/null +++ b/src/modules/evolution/evolution-api.client.ts @@ -0,0 +1,167 @@ +import { Injectable, ServiceUnavailableException } from '@nestjs/common'; +import axios, { AxiosInstance, Method } from 'axios'; + +export interface EvolutionCredentials { + base_url: string; + api_key: string; +} + +export interface EvolutionConnectionInfo { + status_http: number; + versao?: string; + mensagem?: string; +} + +export type EvolutionConnectionState = 'open' | 'connecting' | 'close' | 'desconhecido'; + +export interface EvolutionQrCode { + base64?: string; + pairing_code?: string; +} + +interface EvolutionVersionResponse { + version?: string; + message?: string; +} + +interface EvolutionQrCodeResponse { + base64?: string; + pairingCode?: string; +} + +interface EvolutionConnectionStateResponse { + state?: string; + instance?: { state?: string }; +} + +/** + * Wrapper fino sobre a API REST da Evolution API (WhatsApp), contra a + * especificação pública v2 (`apikey` no header, `/instance/*`, + * `/message/sendText/:instance`). Não há acesso a um servidor Evolution + * real neste ambiente de desenvolvimento (sem internet/instância dedicada) + * — ver Observações da Etapa 4 no controle de progresso. + */ +@Injectable() +export class EvolutionApiClient { + private http(creds: EvolutionCredentials): AxiosInstance { + return axios.create({ + baseURL: creds.base_url.replace(/\/+$/, ''), + timeout: 15_000, + headers: { apikey: creds.api_key }, + validateStatus: () => true, + }); + } + + private async request( + http: AxiosInstance, + method: Method, + url: string, + data?: unknown, + ) { + try { + return await http.request({ method, url, data }); + } catch (error) { + throw new ServiceUnavailableException( + `Falha ao comunicar com a Evolution API: ${(error as Error).message}`, + ); + } + } + + async testarConexao(creds: EvolutionCredentials): Promise { + const response = await this.request(this.http(creds), 'GET', '/'); + if (response.status >= 400) { + throw new ServiceUnavailableException( + `Evolution API respondeu com status ${response.status} ao testar conexão`, + ); + } + return { + status_http: response.status, + versao: response.data?.version, + mensagem: response.data?.message, + }; + } + + async criarInstancia(creds: EvolutionCredentials, instanceName: string): Promise { + const response = await this.request(this.http(creds), 'POST', '/instance/create', { + instanceName, + qrcode: true, + integration: 'WHATSAPP-BAILEYS', + }); + if (response.status >= 400) { + throw new ServiceUnavailableException( + `Evolution API recusou a criação da instância (status ${response.status})`, + ); + } + } + + async gerarQrCode(creds: EvolutionCredentials, instanceName: string): Promise { + const response = await this.request( + this.http(creds), + 'GET', + `/instance/connect/${encodeURIComponent(instanceName)}`, + ); + if (response.status >= 400) { + throw new ServiceUnavailableException( + `Evolution API não retornou QR code (status ${response.status})`, + ); + } + return { + base64: response.data?.base64, + pairing_code: response.data?.pairingCode, + }; + } + + async consultarStatus( + creds: EvolutionCredentials, + instanceName: string, + ): Promise { + const response = await this.request( + this.http(creds), + 'GET', + `/instance/connectionState/${encodeURIComponent(instanceName)}`, + ); + if (response.status >= 400) { + return 'desconhecido'; + } + const state = response.data?.instance?.state ?? response.data?.state; + return state === 'open' || state === 'connecting' || state === 'close' ? state : 'desconhecido'; + } + + async removerInstancia(creds: EvolutionCredentials, instanceName: string): Promise { + const http = this.http(creds); + await this.request( + http, + 'DELETE', + `/instance/logout/${encodeURIComponent(instanceName)}`, + ).catch(() => undefined); + const response = await this.request( + http, + 'DELETE', + `/instance/delete/${encodeURIComponent(instanceName)}`, + ); + if (response.status >= 400 && response.status !== 404) { + throw new ServiceUnavailableException( + `Evolution API recusou a remoção da instância (status ${response.status})`, + ); + } + } + + async enviarMensagem( + creds: EvolutionCredentials, + instanceName: string, + numero: string, + mensagem: string, + ): Promise { + const response = await this.request( + this.http(creds), + 'POST', + `/message/sendText/${encodeURIComponent(instanceName)}`, + { number: numero, text: mensagem }, + ); + if (response.status >= 400) { + throw new ServiceUnavailableException( + `Evolution API recusou o envio da mensagem (status ${response.status})`, + ); + } + } +} diff --git a/src/modules/evolution/evolution.controller.ts b/src/modules/evolution/evolution.controller.ts new file mode 100644 index 0000000..6448907 --- /dev/null +++ b/src/modules/evolution/evolution.controller.ts @@ -0,0 +1,76 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + ParseUUIDPipe, + Post, + Put, + Req, + UseGuards, +} from '@nestjs/common'; +import { Request } from 'express'; +import { CurrentUser } from '../auth/decorators/current-user.decorator'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { MasterGuard } from '../auth/guards/master.guard'; +import { JwtPayload } from '../auth/types/jwt-payload.type'; +import { CreateEvolutionConfigDto } from './dto/create-evolution-config.dto'; +import { UpdateEvolutionConfigDto } from './dto/update-evolution-config.dto'; +import { EvolutionService } from './evolution.service'; + +@UseGuards(JwtAuthGuard, MasterGuard) +@Controller('master/evolution-config') +export class EvolutionController { + constructor(private readonly evolutionService: EvolutionService) {} + + @Post() + create( + @Body() dto: CreateEvolutionConfigDto, + @CurrentUser() user: JwtPayload, + @Req() req: Request, + ) { + return this.evolutionService.create(dto, user, req.ip); + } + + @Get() + findAll() { + return this.evolutionService.findAll(); + } + + @Get(':id') + findOne(@Param('id', ParseUUIDPipe) id: string) { + return this.evolutionService.findOne(id); + } + + @Put(':id') + update( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateEvolutionConfigDto, + @CurrentUser() user: JwtPayload, + @Req() req: Request, + ) { + return this.evolutionService.update(id, dto, user, req.ip); + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + remove( + @Param('id', ParseUUIDPipe) id: string, + @CurrentUser() user: JwtPayload, + @Req() req: Request, + ) { + return this.evolutionService.remove(id, user, req.ip); + } + + @Post(':id/testar-conexao') + testarConexao( + @Param('id', ParseUUIDPipe) id: string, + @CurrentUser() user: JwtPayload, + @Req() req: Request, + ) { + return this.evolutionService.testarConexao(id, user, req.ip); + } +} diff --git a/src/modules/evolution/evolution.module.ts b/src/modules/evolution/evolution.module.ts new file mode 100644 index 0000000..0dcd4bd --- /dev/null +++ b/src/modules/evolution/evolution.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { EvolutionApiClient } from './evolution-api.client'; +import { EvolutionController } from './evolution.controller'; +import { EvolutionService } from './evolution.service'; + +@Module({ + controllers: [EvolutionController], + providers: [EvolutionApiClient, EvolutionService], + exports: [EvolutionApiClient], +}) +export class EvolutionModule {} diff --git a/src/modules/evolution/evolution.service.ts b/src/modules/evolution/evolution.service.ts new file mode 100644 index 0000000..5d62ad3 --- /dev/null +++ b/src/modules/evolution/evolution.service.ts @@ -0,0 +1,151 @@ +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { Prisma, evolution_api_config } from '@prisma/client'; +import { writeAuditLog } from '../../common/audit-log.util'; +import { EncryptionService } from '../../common/encryption.service'; +import { maskSecret } from '../../common/mask.util'; +import { PrismaMasterService } from '../../config/prisma-master.service'; +import { JwtPayload } from '../auth/types/jwt-payload.type'; +import { CreateEvolutionConfigDto } from './dto/create-evolution-config.dto'; +import { UpdateEvolutionConfigDto } from './dto/update-evolution-config.dto'; +import { EvolutionApiClient } from './evolution-api.client'; + +@Injectable() +export class EvolutionService { + constructor( + private readonly prismaMaster: PrismaMasterService, + private readonly encryptionService: EncryptionService, + private readonly evolutionApiClient: EvolutionApiClient, + ) {} + + /** Nunca retorna `api_key_cripto` — só uma versão mascarada (últimos 4 caracteres). */ + private sanitize(config: evolution_api_config) { + const { api_key_cripto, ...rest } = config; + return { ...rest, api_key: maskSecret(this.encryptionService.decrypt(api_key_cripto)) }; + } + + private async findRaw(id: string): Promise { + const config = await this.prismaMaster.evolution_api_config.findUnique({ where: { id } }); + if (!config) { + throw new NotFoundException('Configuração da Evolution API não encontrada'); + } + return config; + } + + async create(dto: CreateEvolutionConfigDto, actor: JwtPayload, ip?: string) { + return this.prismaMaster.$transaction(async (tx) => { + const config = await tx.evolution_api_config.create({ + data: { + nome: dto.nome, + base_url: dto.base_url, + api_key_cripto: this.encryptionService.encrypt(dto.api_key), + ...(dto.versao !== undefined ? { versao: dto.versao } : {}), + }, + }); + + await writeAuditLog(tx, { + usuario_tipo: 'master', + usuario_id: actor.sub, + acao: 'criar_evolution_config', + entidade: 'evolution_api_config', + entidade_id: config.id, + detalhes: { nome: dto.nome, base_url: dto.base_url }, + ip_origem: ip, + }); + + return this.sanitize(config); + }); + } + + async findAll() { + const configs = await this.prismaMaster.evolution_api_config.findMany({ + orderBy: { criado_em: 'asc' }, + }); + return configs.map((config) => this.sanitize(config)); + } + + async findOne(id: string) { + return this.sanitize(await this.findRaw(id)); + } + + async update(id: string, dto: UpdateEvolutionConfigDto, actor: JwtPayload, ip?: string) { + await this.findRaw(id); + + return this.prismaMaster.$transaction(async (tx) => { + const config = await tx.evolution_api_config.update({ + where: { id }, + data: { + ...(dto.nome !== undefined ? { nome: dto.nome } : {}), + ...(dto.base_url !== undefined ? { base_url: dto.base_url } : {}), + ...(dto.api_key !== undefined + ? { api_key_cripto: this.encryptionService.encrypt(dto.api_key) } + : {}), + ...(dto.versao !== undefined ? { versao: dto.versao } : {}), + ...(dto.ativo !== undefined ? { ativo: dto.ativo } : {}), + }, + }); + + await writeAuditLog(tx, { + usuario_tipo: 'master', + usuario_id: actor.sub, + acao: 'atualizar_evolution_config', + entidade: 'evolution_api_config', + entidade_id: id, + detalhes: { + ...dto, + api_key: dto.api_key !== undefined ? '***alterada***' : undefined, + }, + ip_origem: ip, + }); + + return this.sanitize(config); + }); + } + + /** Bloqueia exclusão se alguma instância de WhatsApp ainda referenciar esta config. */ + async remove(id: string, actor: JwtPayload, ip?: string) { + await this.findRaw(id); + + const emUso = await this.prismaMaster.tenant_whatsapp_instances.count({ + where: { evolution_config_id: id }, + }); + if (emUso > 0) { + throw new BadRequestException( + 'Configuração em uso por instância(s) de WhatsApp — remova ou migre as instâncias antes de excluir', + ); + } + + await this.prismaMaster.$transaction(async (tx) => { + await tx.evolution_api_config.delete({ where: { id } }); + + await writeAuditLog(tx, { + usuario_tipo: 'master', + usuario_id: actor.sub, + acao: 'excluir_evolution_config', + entidade: 'evolution_api_config', + entidade_id: id, + ip_origem: ip, + }); + }); + } + + async testarConexao(id: string, actor: JwtPayload, ip?: string) { + const config = await this.findRaw(id); + + const resultado = await this.evolutionApiClient.testarConexao({ + base_url: config.base_url, + api_key: this.encryptionService.decrypt(config.api_key_cripto), + }); + + await writeAuditLog(this.prismaMaster, { + usuario_tipo: 'master', + usuario_id: actor.sub, + acao: 'testar_conexao_evolution', + entidade: 'evolution_api_config', + entidade_id: id, + detalhes: resultado as unknown as Prisma.InputJsonValue, + ip_origem: ip, + }); + + return resultado; + } +} diff --git a/src/modules/health/health.controller.spec.ts b/src/modules/health/health.controller.spec.ts new file mode 100644 index 0000000..d68cbe0 --- /dev/null +++ b/src/modules/health/health.controller.spec.ts @@ -0,0 +1,35 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HealthController } from './health.controller'; +import { HealthService } from './health.service'; + +describe('HealthController', () => { + let controller: HealthController; + const checkMock = jest.fn().mockResolvedValue({ + status: 'ok', + timestamp: new Date().toISOString(), + database: 'up', + }); + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [HealthController], + providers: [ + { + provide: HealthService, + useValue: { check: checkMock }, + }, + ], + }).compile(); + + controller = module.get(HealthController); + }); + + it('deve retornar status ok e database up', async () => { + const result = await controller.check(); + + expect(checkMock).toHaveBeenCalled(); + expect(result.status).toBe('ok'); + expect(result.database).toBe('up'); + expect(result.timestamp).toBeDefined(); + }); +}); diff --git a/src/modules/health/health.controller.ts b/src/modules/health/health.controller.ts new file mode 100644 index 0000000..a26b6ab --- /dev/null +++ b/src/modules/health/health.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { HealthService, HealthStatus } from './health.service'; + +@Controller('health') +export class HealthController { + constructor(private readonly healthService: HealthService) {} + + @Get() + check(): Promise { + return this.healthService.check(); + } +} diff --git a/src/modules/health/health.module.ts b/src/modules/health/health.module.ts new file mode 100644 index 0000000..79af239 --- /dev/null +++ b/src/modules/health/health.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { HealthController } from './health.controller'; +import { HealthService } from './health.service'; + +@Module({ + controllers: [HealthController], + providers: [HealthService], +}) +export class HealthModule {} diff --git a/src/modules/health/health.service.ts b/src/modules/health/health.service.ts new file mode 100644 index 0000000..2062996 --- /dev/null +++ b/src/modules/health/health.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../config/prisma.service'; + +export interface HealthStatus { + status: 'ok' | 'error'; + timestamp: string; + database: 'up' | 'down'; +} + +@Injectable() +export class HealthService { + constructor(private readonly prisma: PrismaService) {} + + async check(): Promise { + const database = await this.checkDatabase(); + + return { + status: database === 'up' ? 'ok' : 'error', + timestamp: new Date().toISOString(), + database, + }; + } + + private async checkDatabase(): Promise<'up' | 'down'> { + try { + await this.prisma.$queryRaw`SELECT 1`; + return 'up'; + } catch { + return 'down'; + } + } +} diff --git a/src/modules/planos/dto/create-plano.dto.ts b/src/modules/planos/dto/create-plano.dto.ts new file mode 100644 index 0000000..291a3b8 --- /dev/null +++ b/src/modules/planos/dto/create-plano.dto.ts @@ -0,0 +1,30 @@ +import { IsInt, IsNumber, IsOptional, IsPositive, IsString, Min, MinLength } from 'class-validator'; + +export class CreatePlanoDto { + @IsString() + @MinLength(2) + nome: string; + + @IsOptional() + @IsString() + descricao?: string; + + @IsNumber() + @IsPositive() + valor_mensal: number; + + @IsOptional() + @IsInt() + @Min(1) + limite_customers?: number; + + @IsOptional() + @IsInt() + @Min(1) + limite_usuarios?: number; + + @IsOptional() + @IsInt() + @Min(1) + limite_instancias_whatsapp?: number; +} diff --git a/src/modules/planos/dto/list-planos.query.dto.ts b/src/modules/planos/dto/list-planos.query.dto.ts new file mode 100644 index 0000000..ea30eb4 --- /dev/null +++ b/src/modules/planos/dto/list-planos.query.dto.ts @@ -0,0 +1,9 @@ +import { Transform } from 'class-transformer'; +import { IsBoolean, IsOptional } from 'class-validator'; + +export class ListPlanosQueryDto { + @IsOptional() + @Transform(({ value }) => (value === undefined ? undefined : value === 'true' || value === true)) + @IsBoolean() + ativo?: boolean; +} diff --git a/src/modules/planos/dto/update-plano.dto.ts b/src/modules/planos/dto/update-plano.dto.ts new file mode 100644 index 0000000..95686da --- /dev/null +++ b/src/modules/planos/dto/update-plano.dto.ts @@ -0,0 +1,45 @@ +import { + IsBoolean, + IsInt, + IsNumber, + IsOptional, + IsPositive, + IsString, + Min, + MinLength, +} from 'class-validator'; + +export class UpdatePlanoDto { + @IsOptional() + @IsString() + @MinLength(2) + nome?: string; + + @IsOptional() + @IsString() + descricao?: string; + + @IsOptional() + @IsNumber() + @IsPositive() + valor_mensal?: number; + + @IsOptional() + @IsInt() + @Min(1) + limite_customers?: number; + + @IsOptional() + @IsInt() + @Min(1) + limite_usuarios?: number; + + @IsOptional() + @IsInt() + @Min(1) + limite_instancias_whatsapp?: number; + + @IsOptional() + @IsBoolean() + ativo?: boolean; +} diff --git a/src/modules/planos/planos.controller.ts b/src/modules/planos/planos.controller.ts new file mode 100644 index 0000000..53e145b --- /dev/null +++ b/src/modules/planos/planos.controller.ts @@ -0,0 +1,62 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + ParseUUIDPipe, + Post, + Put, + Query, + Req, + UseGuards, +} from '@nestjs/common'; +import { Request } from 'express'; +import { CurrentUser } from '../auth/decorators/current-user.decorator'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { MasterGuard } from '../auth/guards/master.guard'; +import { JwtPayload } from '../auth/types/jwt-payload.type'; +import { CreatePlanoDto } from './dto/create-plano.dto'; +import { ListPlanosQueryDto } from './dto/list-planos.query.dto'; +import { UpdatePlanoDto } from './dto/update-plano.dto'; +import { PlanosService } from './planos.service'; + +@UseGuards(JwtAuthGuard, MasterGuard) +@Controller('master/planos') +export class PlanosController { + constructor(private readonly planosService: PlanosService) {} + + @Post() + create(@Body() dto: CreatePlanoDto, @CurrentUser() user: JwtPayload, @Req() req: Request) { + return this.planosService.create(dto, user, req.ip); + } + + @Get() + findAll(@Query() query: ListPlanosQueryDto) { + return this.planosService.findAll(query); + } + + @Get(':id') + findOne(@Param('id', ParseUUIDPipe) id: string) { + return this.planosService.findOne(id); + } + + @Put(':id') + update( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdatePlanoDto, + @CurrentUser() user: JwtPayload, + @Req() req: Request, + ) { + return this.planosService.update(id, dto, user, req.ip); + } + + @Delete(':id') + remove( + @Param('id', ParseUUIDPipe) id: string, + @CurrentUser() user: JwtPayload, + @Req() req: Request, + ) { + return this.planosService.remove(id, user, req.ip); + } +} diff --git a/src/modules/planos/planos.module.ts b/src/modules/planos/planos.module.ts new file mode 100644 index 0000000..f495f86 --- /dev/null +++ b/src/modules/planos/planos.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { PlanosController } from './planos.controller'; +import { PlanosService } from './planos.service'; + +@Module({ + controllers: [PlanosController], + providers: [PlanosService], + exports: [PlanosService], +}) +export class PlanosModule {} diff --git a/src/modules/planos/planos.service.ts b/src/modules/planos/planos.service.ts new file mode 100644 index 0000000..3c932bc --- /dev/null +++ b/src/modules/planos/planos.service.ts @@ -0,0 +1,122 @@ +import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { writeAuditLog } from '../../common/audit-log.util'; +import { PrismaMasterService } from '../../config/prisma-master.service'; +import { JwtPayload } from '../auth/types/jwt-payload.type'; +import { CreatePlanoDto } from './dto/create-plano.dto'; +import { ListPlanosQueryDto } from './dto/list-planos.query.dto'; +import { UpdatePlanoDto } from './dto/update-plano.dto'; + +@Injectable() +export class PlanosService { + constructor(private readonly prismaMaster: PrismaMasterService) {} + + async create(dto: CreatePlanoDto, actor: JwtPayload, ip?: string) { + return this.prismaMaster.$transaction(async (tx) => { + const plano = await tx.planos.create({ data: dto }); + + await writeAuditLog(tx, { + usuario_tipo: 'master', + usuario_id: actor.sub, + acao: 'criar_plano', + entidade: 'planos', + entidade_id: plano.id, + detalhes: dto as unknown as Prisma.InputJsonValue, + ip_origem: ip, + }); + + return plano; + }); + } + + findAll(query: ListPlanosQueryDto) { + return this.prismaMaster.planos.findMany({ + where: query.ativo === undefined ? {} : { ativo: query.ativo }, + orderBy: { criado_em: 'asc' }, + }); + } + + async findOne(id: string) { + const plano = await this.prismaMaster.planos.findUnique({ where: { id } }); + if (!plano) { + throw new NotFoundException('Plano não encontrado'); + } + return plano; + } + + async update(id: string, dto: UpdatePlanoDto, actor: JwtPayload, ip?: string) { + await this.findOne(id); + + return this.prismaMaster.$transaction(async (tx) => { + const plano = await tx.planos.update({ where: { id }, data: dto }); + + await writeAuditLog(tx, { + usuario_tipo: 'master', + usuario_id: actor.sub, + acao: 'atualizar_plano', + entidade: 'planos', + entidade_id: id, + detalhes: dto as unknown as Prisma.InputJsonValue, + ip_origem: ip, + }); + + return plano; + }); + } + + /** + * Exclusão real só é permitida se nenhum tenant não-cancelado estiver + * usando o plano — caso contrário, apenas desativa (`ativo = false`), + * como pedido no critério de aceite ("não permitir excluir plano em uso + * por algum tenant ativo"). + */ + async remove(id: string, actor: JwtPayload, ip?: string) { + const plano = await this.findOne(id); + + const emUso = await this.prismaMaster.tenants.count({ + where: { plano_id: id, status: { not: 'cancelado' } }, + }); + + if (emUso > 0) { + if (!plano.ativo) { + throw new ConflictException( + 'Plano em uso por tenant(s) ativo(s) e já está desativado — não pode ser excluído', + ); + } + + const desativado = await this.prismaMaster.$transaction(async (tx) => { + const atualizado = await tx.planos.update({ where: { id }, data: { ativo: false } }); + + await writeAuditLog(tx, { + usuario_tipo: 'master', + usuario_id: actor.sub, + acao: 'desativar_plano_em_uso', + entidade: 'planos', + entidade_id: id, + detalhes: { motivo: 'plano em uso por tenant(s) ativo(s); exclusão bloqueada' }, + ip_origem: ip, + }); + + return atualizado; + }); + + return { deletado: false, desativado: true, plano: desativado }; + } + + await this.prismaMaster.$transaction(async (tx) => { + await tx.planos.delete({ where: { id } }); + + await writeAuditLog(tx, { + usuario_tipo: 'master', + usuario_id: actor.sub, + acao: 'excluir_plano', + entidade: 'planos', + entidade_id: id, + detalhes: { nome: plano.nome }, + ip_origem: ip, + }); + }); + + return { deletado: true, desativado: false, plano: null }; + } +} diff --git a/src/modules/tenants/dashboard.controller.ts b/src/modules/tenants/dashboard.controller.ts new file mode 100644 index 0000000..b824928 --- /dev/null +++ b/src/modules/tenants/dashboard.controller.ts @@ -0,0 +1,15 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { MasterGuard } from '../auth/guards/master.guard'; +import { DashboardService } from './dashboard.service'; + +@UseGuards(JwtAuthGuard, MasterGuard) +@Controller('master/dashboard') +export class DashboardController { + constructor(private readonly dashboardService: DashboardService) {} + + @Get() + resumo() { + return this.dashboardService.resumo(); + } +} diff --git a/src/modules/tenants/dashboard.service.ts b/src/modules/tenants/dashboard.service.ts new file mode 100644 index 0000000..6dbed9a --- /dev/null +++ b/src/modules/tenants/dashboard.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@nestjs/common'; +import { Prisma, tenant_status } from '@prisma/client'; +import { PrismaMasterService } from '../../config/prisma-master.service'; + +const STATUSES: tenant_status[] = ['trial', 'ativo', 'inadimplente', 'suspenso', 'cancelado']; + +function inicioDoMes(data: Date): Date { + return new Date(Date.UTC(data.getUTCFullYear(), data.getUTCMonth(), 1)); +} + +@Injectable() +export class DashboardService { + constructor(private readonly prismaMaster: PrismaMasterService) {} + + async resumo() { + const porStatusRaw = await this.prismaMaster.tenants.groupBy({ + by: ['status'], + _count: { _all: true }, + }); + const tenants_por_status = Object.fromEntries( + STATUSES.map((status) => [ + status, + porStatusRaw.find((r) => r.status === status)?._count._all ?? 0, + ]), + ); + + // MRR = soma do valor_mensal do plano dos tenants ativos. Não dá pra + // fazer isso com um `aggregate`/`groupBy` do Prisma (soma um campo de + // tabela relacionada), então busca os planos dos tenants ativos e soma + // em memória com `Prisma.Decimal` (evita erro de arredondamento de + // ponto flutuante em valores monetários). + const tenantsAtivos = await this.prismaMaster.tenants.findMany({ + where: { status: 'ativo', plano_id: { not: null } }, + select: { plano: { select: { valor_mensal: true } } }, + }); + const mrr = tenantsAtivos.reduce( + (acc, t) => acc.plus(t.plano?.valor_mensal ?? new Prisma.Decimal(0)), + new Prisma.Decimal(0), + ); + + const inicioMesAtual = inicioDoMes(new Date()); + const inicioProximoMes = new Date( + Date.UTC(inicioMesAtual.getUTCFullYear(), inicioMesAtual.getUTCMonth() + 1, 1), + ); + + const recebidoMes = await this.prismaMaster.tenant_faturas.aggregate({ + _sum: { valor: true }, + where: { status: 'pago', pago_em: { gte: inicioMesAtual, lt: inicioProximoMes } }, + }); + + const emAberto = await this.prismaMaster.tenant_faturas.aggregate({ + _sum: { valor: true }, + where: { status: { in: ['pendente', 'atrasado'] } }, + }); + + return { + tenants_por_status, + mrr: mrr.toFixed(2), + total_recebido_mes: (recebidoMes._sum.valor ?? new Prisma.Decimal(0)).toFixed(2), + total_em_aberto: (emAberto._sum.valor ?? new Prisma.Decimal(0)).toFixed(2), + }; + } +} diff --git a/src/modules/tenants/dto/create-tenant.dto.ts b/src/modules/tenants/dto/create-tenant.dto.ts new file mode 100644 index 0000000..86e9211 --- /dev/null +++ b/src/modules/tenants/dto/create-tenant.dto.ts @@ -0,0 +1,62 @@ +import { Type } from 'class-transformer'; +import { + IsEmail, + IsInt, + IsObject, + IsOptional, + IsString, + IsUUID, + Max, + Min, + MinLength, + ValidateNested, +} from 'class-validator'; + +export class CreateTenantAdminDto { + @IsString() + @MinLength(2) + nome: string; + + @IsEmail() + email: string; + + /** Se omitida, uma senha provisória é gerada e devolvida na resposta (envio por e-mail fica para uma etapa futura). */ + @IsOptional() + @IsString() + senha?: string; +} + +export class CreateTenantDto { + @IsString() + @MinLength(2) + razao_social: string; + + @IsOptional() + @IsString() + nome_fantasia?: string; + + @IsOptional() + @IsString() + documento?: string; + + @IsEmail() + email_contato: string; + + @IsOptional() + @IsString() + telefone?: string; + + @IsUUID() + plano_id: string; + + @IsOptional() + @IsInt() + @Min(1) + @Max(28) + dia_vencimento_mensalidade?: number; + + @IsObject() + @ValidateNested() + @Type(() => CreateTenantAdminDto) + admin: CreateTenantAdminDto; +} diff --git a/src/modules/tenants/dto/gerar-faturas-mes.dto.ts b/src/modules/tenants/dto/gerar-faturas-mes.dto.ts new file mode 100644 index 0000000..1ffd734 --- /dev/null +++ b/src/modules/tenants/dto/gerar-faturas-mes.dto.ts @@ -0,0 +1,8 @@ +import { IsOptional, Matches } from 'class-validator'; + +export class GerarFaturasMesDto { + /** Referência no formato YYYY-MM. Se omitida, usa o mês corrente. */ + @IsOptional() + @Matches(/^\d{4}-(0[1-9]|1[0-2])$/, { message: 'referencia deve estar no formato YYYY-MM' }) + referencia?: string; +} diff --git a/src/modules/tenants/dto/list-faturas.query.dto.ts b/src/modules/tenants/dto/list-faturas.query.dto.ts new file mode 100644 index 0000000..49f8892 --- /dev/null +++ b/src/modules/tenants/dto/list-faturas.query.dto.ts @@ -0,0 +1,33 @@ +import { cobranca_status } from '@prisma/client'; +import { Type } from 'class-transformer'; +import { IsIn, IsInt, IsOptional, IsUUID, Matches, Max, Min } from 'class-validator'; + +const STATUSES: cobranca_status[] = ['pendente', 'pago', 'atrasado', 'cancelado']; + +export class ListFaturasQueryDto { + @IsOptional() + @IsIn(STATUSES) + status?: cobranca_status; + + @IsOptional() + @IsUUID() + tenant_id?: string; + + /** Referência exata no formato YYYY-MM. */ + @IsOptional() + @Matches(/^\d{4}-(0[1-9]|1[0-2])$/, { message: 'referencia deve estar no formato YYYY-MM' }) + referencia?: string; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = 1; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit?: number = 20; +} diff --git a/src/modules/tenants/dto/list-tenants.query.dto.ts b/src/modules/tenants/dto/list-tenants.query.dto.ts new file mode 100644 index 0000000..b024a8d --- /dev/null +++ b/src/modules/tenants/dto/list-tenants.query.dto.ts @@ -0,0 +1,28 @@ +import { tenant_status } from '@prisma/client'; +import { Type } from 'class-transformer'; +import { IsIn, IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; + +const STATUSES: tenant_status[] = ['trial', 'ativo', 'inadimplente', 'suspenso', 'cancelado']; + +export class ListTenantsQueryDto { + @IsOptional() + @IsIn(STATUSES) + status?: tenant_status; + + @IsOptional() + @IsString() + busca?: string; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = 1; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit?: number = 20; +} diff --git a/src/modules/tenants/dto/update-tenant-status.dto.ts b/src/modules/tenants/dto/update-tenant-status.dto.ts new file mode 100644 index 0000000..bd3c16a --- /dev/null +++ b/src/modules/tenants/dto/update-tenant-status.dto.ts @@ -0,0 +1,9 @@ +import { IsIn } from 'class-validator'; +import { tenant_status } from '@prisma/client'; + +const TRANSICOES_PERMITIDAS: tenant_status[] = ['ativo', 'inadimplente', 'suspenso', 'cancelado']; + +export class UpdateTenantStatusDto { + @IsIn(TRANSICOES_PERMITIDAS) + status: tenant_status; +} diff --git a/src/modules/tenants/dto/update-tenant.dto.ts b/src/modules/tenants/dto/update-tenant.dto.ts new file mode 100644 index 0000000..29ac82c --- /dev/null +++ b/src/modules/tenants/dto/update-tenant.dto.ts @@ -0,0 +1,40 @@ +import { IsEmail, IsInt, IsOptional, IsString, IsUUID, Max, Min, MinLength } from 'class-validator'; + +/** + * Atualização de dados cadastrais/plano do tenant. `status` fica de fora de + * propósito — toda transição de status passa por `PATCH /:id/status`, que é + * o único caminho que aplica bloqueio de login e grava a auditoria da + * mudança (ver Observações da Etapa 3 no controle de progresso). + */ +export class UpdateTenantDto { + @IsOptional() + @IsString() + @MinLength(2) + razao_social?: string; + + @IsOptional() + @IsString() + nome_fantasia?: string; + + @IsOptional() + @IsString() + documento?: string; + + @IsOptional() + @IsEmail() + email_contato?: string; + + @IsOptional() + @IsString() + telefone?: string; + + @IsOptional() + @IsUUID() + plano_id?: string; + + @IsOptional() + @IsInt() + @Min(1) + @Max(28) + dia_vencimento_mensalidade?: number; +} diff --git a/src/modules/tenants/faturas.controller.ts b/src/modules/tenants/faturas.controller.ts new file mode 100644 index 0000000..3846c0c --- /dev/null +++ b/src/modules/tenants/faturas.controller.ts @@ -0,0 +1,54 @@ +import { + Body, + Controller, + Get, + HttpCode, + HttpStatus, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, + Req, + UseGuards, +} from '@nestjs/common'; +import { Request } from 'express'; +import { CurrentUser } from '../auth/decorators/current-user.decorator'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { MasterGuard } from '../auth/guards/master.guard'; +import { JwtPayload } from '../auth/types/jwt-payload.type'; +import { GerarFaturasMesDto } from './dto/gerar-faturas-mes.dto'; +import { ListFaturasQueryDto } from './dto/list-faturas.query.dto'; +import { FaturasService } from './faturas.service'; + +@UseGuards(JwtAuthGuard, MasterGuard) +@Controller('master/faturas') +export class FaturasController { + constructor(private readonly faturasService: FaturasService) {} + + @HttpCode(HttpStatus.OK) + @Post('gerar-mes') + gerarMes(@Body() dto: GerarFaturasMesDto, @CurrentUser() user: JwtPayload, @Req() req: Request) { + return this.faturasService.gerarMes(dto, user, req.ip); + } + + @Get() + findAll(@Query() query: ListFaturasQueryDto) { + return this.faturasService.findAll(query); + } + + @Patch(':id/marcar-pago') + marcarPago( + @Param('id', ParseUUIDPipe) id: string, + @CurrentUser() user: JwtPayload, + @Req() req: Request, + ) { + return this.faturasService.marcarPago(id, user, req.ip); + } + + @HttpCode(HttpStatus.OK) + @Post('atualizar-atrasadas') + atualizarAtrasadas(@CurrentUser() user: JwtPayload, @Req() req: Request) { + return this.faturasService.atualizarAtrasadas(user, req.ip); + } +} diff --git a/src/modules/tenants/faturas.service.ts b/src/modules/tenants/faturas.service.ts new file mode 100644 index 0000000..2626603 --- /dev/null +++ b/src/modules/tenants/faturas.service.ts @@ -0,0 +1,177 @@ +import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { writeAuditLog } from '../../common/audit-log.util'; +import { PrismaMasterService } from '../../config/prisma-master.service'; +import { JwtPayload } from '../auth/types/jwt-payload.type'; +import { GerarFaturasMesDto } from './dto/gerar-faturas-mes.dto'; +import { ListFaturasQueryDto } from './dto/list-faturas.query.dto'; + +function referenciaAtual(): string { + const agora = new Date(); + return `${agora.getUTCFullYear()}-${String(agora.getUTCMonth() + 1).padStart(2, '0')}`; +} + +function inicioDoDia(data: Date): Date { + const copia = new Date(data); + copia.setUTCHours(0, 0, 0, 0); + return copia; +} + +@Injectable() +export class FaturasService { + constructor(private readonly prismaMaster: PrismaMasterService) {} + + /** + * Gera, para cada tenant `ativo` com plano associado, uma fatura do mês + * informado (ou o corrente). `skipDuplicates` na `createMany` respeita o + * `UNIQUE(tenant_id, referencia)` sem precisar checar linha a linha — + * chamar de novo no mesmo mês é seguro e idempotente. + */ + async gerarMes(dto: GerarFaturasMesDto, actor: JwtPayload, ip?: string) { + const referencia = dto.referencia ?? referenciaAtual(); + const [ano, mes] = referencia.split('-').map(Number); + + const tenantsAtivos = await this.prismaMaster.tenants.findMany({ + where: { status: 'ativo', plano_id: { not: null } }, + include: { plano: true }, + }); + + if (tenantsAtivos.length === 0) { + return { referencia, criadas: 0, ignoradas: 0 }; + } + + const criadas = await this.prismaMaster.$transaction(async (tx) => { + const resultado = await tx.tenant_faturas.createMany({ + data: tenantsAtivos.map((tenant) => ({ + tenant_id: tenant.id, + referencia, + valor: tenant.plano!.valor_mensal, + vencimento: new Date(Date.UTC(ano, mes - 1, tenant.dia_vencimento_mensalidade)), + })), + skipDuplicates: true, + }); + + await writeAuditLog(tx, { + usuario_tipo: 'master', + usuario_id: actor.sub, + acao: 'gerar_faturas_mes', + entidade: 'tenant_faturas', + detalhes: { + referencia, + tenants_considerados: tenantsAtivos.length, + criadas: resultado.count, + }, + ip_origem: ip, + }); + + return resultado.count; + }); + + return { referencia, criadas, ignoradas: tenantsAtivos.length - criadas }; + } + + async findAll(query: ListFaturasQueryDto) { + const page = query.page ?? 1; + const limit = query.limit ?? 20; + + const where: Prisma.tenant_faturasWhereInput = { + ...(query.status ? { status: query.status } : {}), + ...(query.tenant_id ? { tenant_id: query.tenant_id } : {}), + ...(query.referencia ? { referencia: query.referencia } : {}), + }; + + const [data, total] = await this.prismaMaster.$transaction([ + this.prismaMaster.tenant_faturas.findMany({ + where, + include: { tenant: { select: { id: true, razao_social: true } } }, + orderBy: { vencimento: 'desc' }, + skip: (page - 1) * limit, + take: limit, + }), + this.prismaMaster.tenant_faturas.count({ where }), + ]); + + return { data, meta: { total, page, limit } }; + } + + async marcarPago(id: string, actor: JwtPayload, ip?: string) { + const fatura = await this.prismaMaster.tenant_faturas.findUnique({ where: { id } }); + if (!fatura) { + throw new NotFoundException('Fatura não encontrada'); + } + if (fatura.status === 'pago') { + throw new ConflictException('Fatura já está paga'); + } + + return this.prismaMaster.$transaction(async (tx) => { + const atualizada = await tx.tenant_faturas.update({ + where: { id }, + data: { status: 'pago', pago_em: new Date() }, + }); + + const tenant = await tx.tenants.findUnique({ where: { id: fatura.tenant_id } }); + if (tenant?.status === 'inadimplente') { + await tx.tenants.update({ + where: { id: tenant.id }, + data: { status: 'ativo', atualizado_em: new Date() }, + }); + } + + await writeAuditLog(tx, { + usuario_tipo: 'master', + usuario_id: actor.sub, + tenant_id: fatura.tenant_id, + acao: 'marcar_fatura_paga', + entidade: 'tenant_faturas', + entidade_id: id, + detalhes: { referencia: fatura.referencia, valor: fatura.valor.toString() }, + ip_origem: ip, + }); + + return atualizada; + }); + } + + /** + * Marca como `atrasado` toda fatura `pendente` vencida e, para os + * tenants `ativo` afetados, muda o status para `inadimplente`. Tenants já + * `suspenso`/`cancelado` não são reativados como `inadimplente` por isso. + */ + async atualizarAtrasadas(actor: JwtPayload, ip?: string) { + const hoje = inicioDoDia(new Date()); + + const vencidas = await this.prismaMaster.tenant_faturas.findMany({ + where: { status: 'pendente', vencimento: { lt: hoje } }, + select: { id: true, tenant_id: true }, + }); + + if (vencidas.length === 0) { + return { atualizadas: 0, tenants_afetados: 0 }; + } + + const tenantIds = [...new Set(vencidas.map((f) => f.tenant_id))]; + + return this.prismaMaster.$transaction(async (tx) => { + await tx.tenant_faturas.updateMany({ + where: { id: { in: vencidas.map((f) => f.id) } }, + data: { status: 'atrasado' }, + }); + + await tx.tenants.updateMany({ + where: { id: { in: tenantIds }, status: 'ativo' }, + data: { status: 'inadimplente', atualizado_em: new Date() }, + }); + + await writeAuditLog(tx, { + usuario_tipo: 'master', + usuario_id: actor.sub, + acao: 'atualizar_faturas_atrasadas', + entidade: 'tenant_faturas', + detalhes: { quantidade: vencidas.length, tenant_ids: tenantIds }, + ip_origem: ip, + }); + + return { atualizadas: vencidas.length, tenants_afetados: tenantIds.length }; + }); + } +} diff --git a/src/modules/tenants/provisional-password.util.ts b/src/modules/tenants/provisional-password.util.ts new file mode 100644 index 0000000..cfacf8d --- /dev/null +++ b/src/modules/tenants/provisional-password.util.ts @@ -0,0 +1,6 @@ +import { randomBytes } from 'node:crypto'; + +/** Senha provisória para o primeiro `tenant_admin` de um tenant recém-criado, quando não informada no payload. */ +export function generateProvisionalPassword(): string { + return randomBytes(9).toString('base64url'); +} diff --git a/src/modules/tenants/tenants.controller.ts b/src/modules/tenants/tenants.controller.ts new file mode 100644 index 0000000..4b538dd --- /dev/null +++ b/src/modules/tenants/tenants.controller.ts @@ -0,0 +1,64 @@ +import { + Body, + Controller, + Get, + Param, + ParseUUIDPipe, + Patch, + Post, + Put, + Query, + Req, + UseGuards, +} from '@nestjs/common'; +import { Request } from 'express'; +import { CurrentUser } from '../auth/decorators/current-user.decorator'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { MasterGuard } from '../auth/guards/master.guard'; +import { JwtPayload } from '../auth/types/jwt-payload.type'; +import { CreateTenantDto } from './dto/create-tenant.dto'; +import { ListTenantsQueryDto } from './dto/list-tenants.query.dto'; +import { UpdateTenantDto } from './dto/update-tenant.dto'; +import { UpdateTenantStatusDto } from './dto/update-tenant-status.dto'; +import { TenantsService } from './tenants.service'; + +@UseGuards(JwtAuthGuard, MasterGuard) +@Controller('master/tenants') +export class TenantsController { + constructor(private readonly tenantsService: TenantsService) {} + + @Post() + create(@Body() dto: CreateTenantDto, @CurrentUser() user: JwtPayload, @Req() req: Request) { + return this.tenantsService.create(dto, user, req.ip); + } + + @Get() + findAll(@Query() query: ListTenantsQueryDto) { + return this.tenantsService.findAll(query); + } + + @Get(':id') + findOne(@Param('id', ParseUUIDPipe) id: string) { + return this.tenantsService.findOne(id); + } + + @Put(':id') + update( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateTenantDto, + @CurrentUser() user: JwtPayload, + @Req() req: Request, + ) { + return this.tenantsService.update(id, dto, user, req.ip); + } + + @Patch(':id/status') + changeStatus( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateTenantStatusDto, + @CurrentUser() user: JwtPayload, + @Req() req: Request, + ) { + return this.tenantsService.changeStatus(id, dto.status, user, req.ip); + } +} diff --git a/src/modules/tenants/tenants.module.ts b/src/modules/tenants/tenants.module.ts new file mode 100644 index 0000000..8845d93 --- /dev/null +++ b/src/modules/tenants/tenants.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { DashboardController } from './dashboard.controller'; +import { DashboardService } from './dashboard.service'; +import { FaturasController } from './faturas.controller'; +import { FaturasService } from './faturas.service'; +import { TenantsController } from './tenants.controller'; +import { TenantsService } from './tenants.service'; + +@Module({ + controllers: [TenantsController, FaturasController, DashboardController], + providers: [TenantsService, FaturasService, DashboardService], + exports: [TenantsService, FaturasService], +}) +export class TenantsModule {} diff --git a/src/modules/tenants/tenants.service.ts b/src/modules/tenants/tenants.service.ts new file mode 100644 index 0000000..5c9a848 --- /dev/null +++ b/src/modules/tenants/tenants.service.ts @@ -0,0 +1,223 @@ +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Prisma, tenant_status } from '@prisma/client'; +import { writeAuditLog } from '../../common/audit-log.util'; +import { PrismaMasterService } from '../../config/prisma-master.service'; +import { assertPasswordStrength, hashPassword } from '../auth/password.util'; +import { TENANT_BLOCKED_STATUSES } from '../auth/auth.service'; +import { JwtPayload } from '../auth/types/jwt-payload.type'; +import { CreateTenantDto } from './dto/create-tenant.dto'; +import { ListTenantsQueryDto } from './dto/list-tenants.query.dto'; +import { UpdateTenantDto } from './dto/update-tenant.dto'; +import { generateProvisionalPassword } from './provisional-password.util'; + +@Injectable() +export class TenantsService { + private readonly trialDias: number; + + constructor( + private readonly prismaMaster: PrismaMasterService, + private readonly configService: ConfigService, + ) { + this.trialDias = this.configService.get('TENANT_TRIAL_DIAS', 14); + } + + async create(dto: CreateTenantDto, actor: JwtPayload, ip?: string) { + const plano = await this.prismaMaster.planos.findUnique({ where: { id: dto.plano_id } }); + if (!plano || !plano.ativo) { + throw new BadRequestException('Plano informado não existe ou está inativo'); + } + + const senhaProvisoria = dto.admin.senha ?? generateProvisionalPassword(); + assertPasswordStrength(senhaProvisoria); + const senha_hash = await hashPassword(senhaProvisoria); + + const trial_expira_em = new Date(Date.now() + this.trialDias * 24 * 60 * 60 * 1000); + + const { tenant, admin } = await this.prismaMaster.$transaction(async (tx) => { + const tenantCriado = await tx.tenants.create({ + data: { + razao_social: dto.razao_social, + nome_fantasia: dto.nome_fantasia, + documento: dto.documento, + email_contato: dto.email_contato, + telefone: dto.telefone, + plano_id: dto.plano_id, + status: 'trial', + trial_expira_em, + criado_por: actor.sub, + ...(dto.dia_vencimento_mensalidade !== undefined + ? { dia_vencimento_mensalidade: dto.dia_vencimento_mensalidade } + : {}), + }, + }); + + const adminCriado = await tx.tenant_users.create({ + data: { + tenant_id: tenantCriado.id, + nome: dto.admin.nome, + email: dto.admin.email, + senha_hash, + role: 'tenant_admin', + }, + }); + + await writeAuditLog(tx, { + usuario_tipo: 'master', + usuario_id: actor.sub, + tenant_id: tenantCriado.id, + acao: 'criar_tenant', + entidade: 'tenants', + entidade_id: tenantCriado.id, + detalhes: { plano_id: dto.plano_id, admin_email: dto.admin.email }, + ip_origem: ip, + }); + + return { tenant: tenantCriado, admin: adminCriado }; + }); + + return { + tenant, + admin: { + id: admin.id, + nome: admin.nome, + email: admin.email, + senha_provisoria: senhaProvisoria, + }, + }; + } + + async findAll(query: ListTenantsQueryDto) { + const page = query.page ?? 1; + const limit = query.limit ?? 20; + + const where: Prisma.tenantsWhereInput = { + ...(query.status ? { status: query.status } : {}), + ...(query.busca + ? { + OR: [ + { razao_social: { contains: query.busca, mode: 'insensitive' } }, + { nome_fantasia: { contains: query.busca, mode: 'insensitive' } }, + { documento: { contains: query.busca, mode: 'insensitive' } }, + ], + } + : {}), + }; + + const [data, total] = await this.prismaMaster.$transaction([ + this.prismaMaster.tenants.findMany({ + where, + include: { plano: { select: { id: true, nome: true, valor_mensal: true } } }, + orderBy: { criado_em: 'desc' }, + skip: (page - 1) * limit, + take: limit, + }), + this.prismaMaster.tenants.count({ where }), + ]); + + return { data, meta: { total, page, limit } }; + } + + async findOne(id: string) { + const tenant = await this.prismaMaster.tenants.findUnique({ + where: { id }, + include: { + plano: true, + faturas: { orderBy: { criado_em: 'desc' }, take: 5 }, + _count: { select: { usuarios: true, customers: true, vendas: true } }, + }, + }); + + if (!tenant) { + throw new NotFoundException('Tenant não encontrado'); + } + + return tenant; + } + + async update(id: string, dto: UpdateTenantDto, actor: JwtPayload, ip?: string) { + await this.findOne(id); + + if (dto.plano_id) { + const plano = await this.prismaMaster.planos.findUnique({ where: { id: dto.plano_id } }); + if (!plano || !plano.ativo) { + throw new BadRequestException('Plano informado não existe ou está inativo'); + } + } + + return this.prismaMaster.$transaction(async (tx) => { + const atualizado = await tx.tenants.update({ + where: { id }, + data: { ...dto, atualizado_em: new Date() }, + }); + + await writeAuditLog(tx, { + usuario_tipo: 'master', + usuario_id: actor.sub, + tenant_id: id, + acao: 'atualizar_tenant', + entidade: 'tenants', + entidade_id: id, + detalhes: dto as unknown as Prisma.InputJsonValue, + ip_origem: ip, + }); + + return atualizado; + }); + } + + /** + * Único caminho para mudar `tenants.status`. Além de gravar a auditoria, + * ao transicionar para `suspenso`/`cancelado` revoga (defesa em + * profundidade, mesmo espírito da revogação de família de refresh tokens + * da Etapa 2) os refresh tokens ativos dos `tenant_users` do tenant — o + * bloqueio "duro" em si é feito pelo `TenantGuard`, que checa o status a + * cada request, não só no login. + */ + async changeStatus(id: string, novoStatus: tenant_status, actor: JwtPayload, ip?: string) { + const tenant = await this.prismaMaster.tenants.findUnique({ where: { id } }); + if (!tenant) { + throw new NotFoundException('Tenant não encontrado'); + } + + const statusAnterior = tenant.status; + + return this.prismaMaster.$transaction(async (tx) => { + const atualizado = await tx.tenants.update({ + where: { id }, + data: { status: novoStatus, atualizado_em: new Date() }, + }); + + if (TENANT_BLOCKED_STATUSES.has(novoStatus)) { + const usuarios = await tx.tenant_users.findMany({ + where: { tenant_id: id }, + select: { id: true }, + }); + + if (usuarios.length > 0) { + await tx.auth_refresh_tokens.updateMany({ + where: { + usuario_tipo: 'tenant_user', + usuario_id: { in: usuarios.map((u) => u.id) }, + revogado: false, + }, + data: { revogado: true }, + }); + } + } + + await writeAuditLog(tx, { + usuario_tipo: 'master', + usuario_id: actor.sub, + tenant_id: id, + acao: 'mudar_status_tenant', + entidade: 'tenants', + entidade_id: id, + detalhes: { de: statusAnterior, para: novoStatus }, + ip_origem: ip, + }); + + return atualizado; + }); + } +} diff --git a/src/modules/whatsapp/dto/testar-envio-whatsapp.dto.ts b/src/modules/whatsapp/dto/testar-envio-whatsapp.dto.ts new file mode 100644 index 0000000..3ca83d2 --- /dev/null +++ b/src/modules/whatsapp/dto/testar-envio-whatsapp.dto.ts @@ -0,0 +1,14 @@ +import { IsOptional, IsString, Matches, MinLength } from 'class-validator'; + +export class TestarEnvioWhatsappDto { + @IsString() + @Matches(/^\d{10,15}$/, { + message: 'numero deve conter apenas dígitos (DDI+DDD+número), de 10 a 15 caracteres', + }) + numero: string; + + @IsOptional() + @IsString() + @MinLength(1) + mensagem?: string; +} diff --git a/src/modules/whatsapp/instance-name.util.ts b/src/modules/whatsapp/instance-name.util.ts new file mode 100644 index 0000000..e9a1de4 --- /dev/null +++ b/src/modules/whatsapp/instance-name.util.ts @@ -0,0 +1,51 @@ +import { randomBytes } from 'node:crypto'; + +const ACENTOS: Record = { + á: 'a', + à: 'a', + â: 'a', + ã: 'a', + ä: 'a', + é: 'e', + è: 'e', + ê: 'e', + ë: 'e', + í: 'i', + ì: 'i', + î: 'i', + ï: 'i', + ó: 'o', + ò: 'o', + ô: 'o', + õ: 'o', + ö: 'o', + ú: 'u', + ù: 'u', + û: 'u', + ü: 'u', + ç: 'c', + ñ: 'n', +}; + +function removerAcentos(value: string): string { + return value + .split('') + .map((char) => ACENTOS[char.toLowerCase()] ?? char) + .join(''); +} + +function slugify(value: string): string { + const slug = removerAcentos(value) + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/(^-+|-+$)/g, '') + .slice(0, 30); + + return slug || 'tenant'; +} + +/** Nome sugerido para uma nova instância, ex: `tenant-clinica-boa-saude-a1b2c3d4`. */ +export function generateInstanceName(referencia: string): string { + const sufixo = randomBytes(4).toString('hex'); + return `tenant-${slugify(referencia)}-${sufixo}`; +} diff --git a/src/modules/whatsapp/whatsapp-status.util.ts b/src/modules/whatsapp/whatsapp-status.util.ts new file mode 100644 index 0000000..b99e1cb --- /dev/null +++ b/src/modules/whatsapp/whatsapp-status.util.ts @@ -0,0 +1,16 @@ +import { EvolutionConnectionState } from '../evolution/evolution-api.client'; + +export type WhatsappInstanceStatus = 'desconectado' | 'conectando' | 'conectado' | 'erro'; + +export function mapEvolutionState(state: EvolutionConnectionState): WhatsappInstanceStatus { + switch (state) { + case 'open': + return 'conectado'; + case 'connecting': + return 'conectando'; + case 'close': + return 'desconectado'; + default: + return 'erro'; + } +} diff --git a/src/modules/whatsapp/whatsapp.controller.ts b/src/modules/whatsapp/whatsapp.controller.ts new file mode 100644 index 0000000..c6f46ec --- /dev/null +++ b/src/modules/whatsapp/whatsapp.controller.ts @@ -0,0 +1,62 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + ParseUUIDPipe, + Post, + UseGuards, +} from '@nestjs/common'; +import { CurrentUser } from '../auth/decorators/current-user.decorator'; +import { Roles } from '../auth/decorators/roles.decorator'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { RolesGuard } from '../auth/guards/roles.guard'; +import { TenantGuard } from '../auth/guards/tenant.guard'; +import { JwtPayload } from '../auth/types/jwt-payload.type'; +import { TestarEnvioWhatsappDto } from './dto/testar-envio-whatsapp.dto'; +import { WhatsappService } from './whatsapp.service'; + +@UseGuards(JwtAuthGuard, TenantGuard, RolesGuard) +@Roles('tenant_admin') +@Controller('tenant/whatsapp/instancias') +export class WhatsappController { + constructor(private readonly whatsappService: WhatsappService) {} + + @Post() + criar(@CurrentUser() user: JwtPayload) { + return this.whatsappService.criarInstancia(user.tenant_id as string); + } + + @Get() + listar() { + return this.whatsappService.listar(); + } + + @Get(':id/qrcode') + qrcode(@Param('id', ParseUUIDPipe) id: string) { + return this.whatsappService.obterQrCode(id); + } + + @Get(':id/status') + status(@Param('id', ParseUUIDPipe) id: string) { + return this.whatsappService.consultarStatus(id); + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + remover(@Param('id', ParseUUIDPipe) id: string) { + return this.whatsappService.remover(id); + } + + @Post(':id/testar-envio') + testarEnvio( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: TestarEnvioWhatsappDto, + @CurrentUser() user: JwtPayload, + ) { + return this.whatsappService.testarEnvio(user.tenant_id as string, id, dto); + } +} diff --git a/src/modules/whatsapp/whatsapp.module.ts b/src/modules/whatsapp/whatsapp.module.ts new file mode 100644 index 0000000..a0883ed --- /dev/null +++ b/src/modules/whatsapp/whatsapp.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { AlertDispatchModule } from '../alert-dispatch/alert-dispatch.module'; +import { EvolutionModule } from '../evolution/evolution.module'; +import { WhatsappController } from './whatsapp.controller'; +import { WhatsappService } from './whatsapp.service'; + +@Module({ + imports: [EvolutionModule, AlertDispatchModule], + controllers: [WhatsappController], + providers: [WhatsappService], +}) +export class WhatsappModule {} diff --git a/src/modules/whatsapp/whatsapp.service.ts b/src/modules/whatsapp/whatsapp.service.ts new file mode 100644 index 0000000..79b12f6 --- /dev/null +++ b/src/modules/whatsapp/whatsapp.service.ts @@ -0,0 +1,171 @@ +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { evolution_api_config, tenant_whatsapp_instances } from '@prisma/client'; +import { EncryptionService } from '../../common/encryption.service'; +import { PrismaMasterService } from '../../config/prisma-master.service'; +import { TenantContextService } from '../../config/tenant-context.service'; +import { AlertDispatchService } from '../alert-dispatch/alert-dispatch.service'; +import { EvolutionApiClient, EvolutionCredentials } from '../evolution/evolution-api.client'; +import { TestarEnvioWhatsappDto } from './dto/testar-envio-whatsapp.dto'; +import { generateInstanceName } from './instance-name.util'; +import { mapEvolutionState } from './whatsapp-status.util'; + +const MENSAGEM_TESTE_PADRAO = 'Mensagem de teste do SeConectai Vendas.'; +const MAX_TENTATIVAS_NOME_UNICO = 5; + +@Injectable() +export class WhatsappService { + constructor( + private readonly prismaMaster: PrismaMasterService, + private readonly tenantContext: TenantContextService, + private readonly encryptionService: EncryptionService, + private readonly evolutionApiClient: EvolutionApiClient, + private readonly alertDispatchService: AlertDispatchService, + ) {} + + private async resolverConfigAtiva(): Promise { + const config = await this.prismaMaster.evolution_api_config.findFirst({ + where: { ativo: true }, + orderBy: { criado_em: 'asc' }, + }); + if (!config) { + throw new BadRequestException('Nenhuma configuração da Evolution API ativa'); + } + return config; + } + + private credenciais(config: evolution_api_config): EvolutionCredentials { + return { + base_url: config.base_url, + api_key: this.encryptionService.decrypt(config.api_key_cripto), + }; + } + + private async buscarInstanciaDoTenant(instanceId: string): Promise { + const tx = this.tenantContext.getTx(); + const instance = await tx.tenant_whatsapp_instances.findUnique({ where: { id: instanceId } }); + if (!instance) { + throw new NotFoundException('Instância de WhatsApp não encontrada'); + } + return instance; + } + + private async buscarConfigDaInstancia( + instance: Pick, + ): Promise { + if (!instance.evolution_config_id) { + throw new BadRequestException('Instância sem configuração da Evolution API associada'); + } + const config = await this.prismaMaster.evolution_api_config.findUnique({ + where: { id: instance.evolution_config_id }, + }); + if (!config) { + throw new BadRequestException('Configuração da Evolution API associada não encontrada'); + } + return config; + } + + async listar(): Promise { + return this.tenantContext + .getTx() + .tenant_whatsapp_instances.findMany({ orderBy: { criado_em: 'desc' } }); + } + + async criarInstancia(tenantId: string): Promise { + const tenant = await this.prismaMaster.tenants.findUnique({ + where: { id: tenantId }, + include: { plano: true }, + }); + if (!tenant?.plano) { + throw new BadRequestException('Tenant sem plano associado'); + } + + const tx = this.tenantContext.getTx(); + const existentes = await tx.tenant_whatsapp_instances.count(); + if (existentes >= tenant.plano.limite_instancias_whatsapp) { + throw new BadRequestException( + `Limite de ${tenant.plano.limite_instancias_whatsapp} instância(s) de WhatsApp do plano atingido`, + ); + } + + const config = await this.resolverConfigAtiva(); + const referencia = tenant.nome_fantasia ?? tenant.razao_social; + + let instanceName = generateInstanceName(referencia); + for (let tentativa = 0; tentativa < MAX_TENTATIVAS_NOME_UNICO; tentativa++) { + const emUso = await this.prismaMaster.tenant_whatsapp_instances.count({ + where: { instance_name: instanceName }, + }); + if (emUso === 0) { + break; + } + instanceName = generateInstanceName(referencia); + } + + await this.evolutionApiClient.criarInstancia(this.credenciais(config), instanceName); + + return tx.tenant_whatsapp_instances.create({ + data: { + tenant_id: tenantId, + evolution_config_id: config.id, + instance_name: instanceName, + status: 'desconectado', + }, + }); + } + + async obterQrCode(instanceId: string) { + const instance = await this.buscarInstanciaDoTenant(instanceId); + const config = await this.buscarConfigDaInstancia(instance); + + const qrcode = await this.evolutionApiClient.gerarQrCode( + this.credenciais(config), + instance.instance_name, + ); + + await this.tenantContext.getTx().tenant_whatsapp_instances.update({ + where: { id: instance.id }, + data: { qr_code_gerado_em: new Date(), status: 'conectando' }, + }); + + return qrcode; + } + + async consultarStatus(instanceId: string): Promise { + const instance = await this.buscarInstanciaDoTenant(instanceId); + const config = await this.buscarConfigDaInstancia(instance); + + const estado = await this.evolutionApiClient.consultarStatus( + this.credenciais(config), + instance.instance_name, + ); + + return this.tenantContext.getTx().tenant_whatsapp_instances.update({ + where: { id: instance.id }, + data: { status: mapEvolutionState(estado) }, + }); + } + + async remover(instanceId: string): Promise { + const instance = await this.buscarInstanciaDoTenant(instanceId); + const config = await this.buscarConfigDaInstancia(instance); + + await this.evolutionApiClient.removerInstancia( + this.credenciais(config), + instance.instance_name, + ); + await this.tenantContext + .getTx() + .tenant_whatsapp_instances.delete({ where: { id: instance.id } }); + } + + async testarEnvio(tenantId: string, instanceId: string, dto: TestarEnvioWhatsappDto) { + await this.buscarInstanciaDoTenant(instanceId); + + return this.alertDispatchService.enviarWhatsapp( + tenantId, + dto.numero, + dto.mensagem ?? MENSAGEM_TESTE_PADRAO, + instanceId, + ); + } +} diff --git a/test/auth.e2e-spec.ts b/test/auth.e2e-spec.ts new file mode 100644 index 0000000..8326305 --- /dev/null +++ b/test/auth.e2e-spec.ts @@ -0,0 +1,440 @@ +import { randomUUID } from 'node:crypto'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; +import { PrismaService } from '../src/config/prisma.service'; +import { hashPassword } from '../src/modules/auth/password.util'; + +interface TokenPair { + access_token: string; + refresh_token: string; + token_type: string; + expires_in: number; +} + +describe('Auth (e2e)', () => { + let app: INestApplication; + let prisma: PrismaService; + let httpServer: Parameters[0]; + + const senha = 'SenhaForteDemo#26'; + const suffix = randomUUID().slice(0, 8); + + const masterEmail = `master-${suffix}@teste.com`; + const tenantAAdminEmail = `admin-a-${suffix}@teste.com`; + const tenantAVendedorEmail = `vendedor-a-${suffix}@teste.com`; + const tenantBAdminEmail = `admin-b-${suffix}@teste.com`; + + let masterId: string; + let tenantAId: string; + let tenantBId: string; + let tenantAAdminId: string; + let tenantAVendedorId: string; + let tenantBAdminId: string; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }), + ); + await app.init(); + httpServer = app.getHttpServer() as Parameters[0]; + + prisma = app.get(PrismaService); + + const senha_hash = await hashPassword(senha); + + const master = await prisma.master_admins.create({ + data: { nome: 'Master Teste', email: masterEmail, senha_hash }, + }); + masterId = master.id; + + const tenantA = await prisma.tenants.create({ + data: { + razao_social: `Tenant A ${suffix}`, + email_contato: `contato-a-${suffix}@teste.com`, + status: 'ativo', + }, + }); + tenantAId = tenantA.id; + + const tenantB = await prisma.tenants.create({ + data: { + razao_social: `Tenant B ${suffix}`, + email_contato: `contato-b-${suffix}@teste.com`, + status: 'ativo', + }, + }); + tenantBId = tenantB.id; + + const tenantAAdmin = await prisma.tenant_users.create({ + data: { + tenant_id: tenantAId, + nome: 'Admin Tenant A', + email: tenantAAdminEmail, + senha_hash, + role: 'tenant_admin', + }, + }); + tenantAAdminId = tenantAAdmin.id; + + const tenantAVendedor = await prisma.tenant_users.create({ + data: { + tenant_id: tenantAId, + nome: 'Vendedor Tenant A', + email: tenantAVendedorEmail, + senha_hash, + role: 'vendedor', + }, + }); + tenantAVendedorId = tenantAVendedor.id; + + const tenantBAdmin = await prisma.tenant_users.create({ + data: { + tenant_id: tenantBId, + nome: 'Admin Tenant B', + email: tenantBAdminEmail, + senha_hash, + role: 'tenant_admin', + }, + }); + tenantBAdminId = tenantBAdmin.id; + }); + + afterAll(async () => { + const usuarioIds = [masterId, tenantAAdminId, tenantAVendedorId, tenantBAdminId]; + const emails = [masterEmail, tenantAAdminEmail, tenantAVendedorEmail, tenantBAdminEmail]; + + await prisma.auth_refresh_tokens.deleteMany({ where: { usuario_id: { in: usuarioIds } } }); + await prisma.password_reset_tokens.deleteMany({ where: { usuario_id: { in: usuarioIds } } }); + await prisma.login_attempts.deleteMany({ where: { email: { in: emails } } }); + await prisma.tenant_users.deleteMany({ where: { tenant_id: { in: [tenantAId, tenantBId] } } }); + await prisma.tenants.deleteMany({ where: { id: { in: [tenantAId, tenantBId] } } }); + await prisma.master_admins.deleteMany({ where: { id: masterId } }); + + await app.close(); + }); + + describe('login', () => { + it('login master com credenciais corretas retorna access+refresh token', async () => { + const res = await request(httpServer) + .post('/auth/master/login') + .send({ email: masterEmail, senha }) + .expect(200); + + const body = res.body as TokenPair; + expect(body.access_token).toEqual(expect.any(String)); + expect(body.refresh_token).toEqual(expect.any(String)); + expect(body.token_type).toBe('Bearer'); + }); + + it('login tenant com credenciais corretas retorna access+refresh token com tenant_id/role no payload', async () => { + const res = await request(httpServer) + .post('/auth/tenant/login') + .send({ email: tenantAAdminEmail, senha }) + .expect(200); + + const body = res.body as TokenPair; + expect(body.access_token).toEqual(expect.any(String)); + expect(body.refresh_token).toEqual(expect.any(String)); + + const me = await request(httpServer) + .get('/auth/me') + .set('Authorization', `Bearer ${body.access_token}`) + .expect(200); + + expect(me.body).toMatchObject({ + tipo: 'tenant_user', + tenant_id: tenantAId, + role: 'tenant_admin', + sub: tenantAAdminId, + }); + }); + + it('senha incorreta retorna 401', async () => { + await request(httpServer) + .post('/auth/tenant/login') + .send({ email: tenantAAdminEmail, senha: 'senha-errada-qualquer' }) + .expect(401); + }); + + it('e-mail inexistente retorna 401 (sem vazar se o e-mail existe)', async () => { + await request(httpServer) + .post('/auth/master/login') + .send({ email: 'nao-existe-nenhum@teste.com', senha }) + .expect(401); + }); + }); + + describe('refresh token', () => { + it('rotaciona corretamente; token antigo é rejeitado após rotação', async () => { + const login = await request(httpServer) + .post('/auth/tenant/login') + .send({ email: tenantAVendedorEmail, senha }) + .expect(200); + + const originalRefreshToken = (login.body as TokenPair).refresh_token; + + const refreshed = await request(httpServer) + .post('/auth/refresh') + .send({ refresh_token: originalRefreshToken }) + .expect(200); + + const newTokens = refreshed.body as TokenPair; + expect(newTokens.refresh_token).not.toBe(originalRefreshToken); + expect(newTokens.access_token).toEqual(expect.any(String)); + + // o novo refresh token, por sua vez, ainda é válido (rotaciona de novo normalmente) + const secondRotation = await request(httpServer) + .post('/auth/refresh') + .send({ refresh_token: newTokens.refresh_token }) + .expect(200); + expect((secondRotation.body as TokenPair).refresh_token).not.toBe(newTokens.refresh_token); + }); + + it('token antigo (já rotacionado) usado de novo é rejeitado — e, por segurança, revoga toda a família de tokens', async () => { + const login = await request(httpServer) + .post('/auth/tenant/login') + .send({ email: tenantAAdminEmail, senha }) + .expect(200); + + const originalRefreshToken = (login.body as TokenPair).refresh_token; + + const refreshed = await request(httpServer) + .post('/auth/refresh') + .send({ refresh_token: originalRefreshToken }) + .expect(200); + const rotatedRefreshToken = (refreshed.body as TokenPair).refresh_token; + + // reusar o token antigo (já revogado pela rotação) é rejeitado + await request(httpServer) + .post('/auth/refresh') + .send({ refresh_token: originalRefreshToken }) + .expect(401); + + // esse reuso é tratado como indício de roubo de token: por precaução, + // toda a família (incluindo o token rotacionado válido) é revogada + await request(httpServer) + .post('/auth/refresh') + .send({ refresh_token: rotatedRefreshToken }) + .expect(401); + }); + + it('refresh token inválido/inexistente retorna 401', async () => { + await request(httpServer) + .post('/auth/refresh') + .send({ refresh_token: 'token-que-nao-existe' }) + .expect(401); + }); + }); + + describe('logout', () => { + it('revoga o refresh token — uso após logout é rejeitado', async () => { + const login = await request(httpServer) + .post('/auth/tenant/login') + .send({ email: tenantAAdminEmail, senha }) + .expect(200); + + const { refresh_token } = login.body as TokenPair; + + await request(httpServer).post('/auth/logout').send({ refresh_token }).expect(204); + + await request(httpServer).post('/auth/refresh').send({ refresh_token }).expect(401); + }); + }); + + describe('esqueci minha senha / reset', () => { + it('gera token de uso único, com expiração, e permite redefinir a senha', async () => { + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined); + + await request(httpServer) + .post('/auth/password/forgot') + .send({ email: tenantAVendedorEmail, usuario_tipo: 'tenant_user' }) + .expect(200); + + const logLine = consoleSpy.mock.calls + .map((args) => String(args[0])) + .find((line) => line.includes('token=')); + consoleSpy.mockRestore(); + + expect(logLine).toBeDefined(); + const token = /token=(\S+)/.exec(logLine ?? '')?.[1]; + expect(token).toBeTruthy(); + + const novaSenha = 'NovaSenhaForte#26'; + + await request(httpServer) + .post('/auth/password/reset') + .send({ token, nova_senha: novaSenha }) + .expect(200); + + // token de uso único: reutilizar deve falhar + await request(httpServer) + .post('/auth/password/reset') + .send({ token, nova_senha: 'OutraSenha#26' }) + .expect(401); + + // senha antiga não funciona mais + await request(httpServer) + .post('/auth/tenant/login') + .send({ email: tenantAVendedorEmail, senha }) + .expect(401); + + // senha nova funciona + await request(httpServer) + .post('/auth/tenant/login') + .send({ email: tenantAVendedorEmail, senha: novaSenha }) + .expect(200); + + // deixa a senha como estava para não afetar outros testes que rodem depois + await prisma.tenant_users.update({ + where: { id: tenantAVendedorId }, + data: { senha_hash: await hashPassword(senha) }, + }); + }); + + it('token inválido retorna 401', async () => { + await request(httpServer) + .post('/auth/password/reset') + .send({ token: 'token-invalido', nova_senha: 'QualquerSenha#26' }) + .expect(401); + }); + + it('senha fraca no reset é rejeitada (400) antes de validar o token', async () => { + await request(httpServer) + .post('/auth/password/reset') + .send({ token: 'token-invalido', nova_senha: '123' }) + .expect(400); + }); + }); + + describe('guards: MasterGuard / TenantGuard / RolesGuard', () => { + it('sem token retorna 401', async () => { + await request(httpServer).get('/auth/me').expect(401); + }); + + it('token de tenant não acessa rota master (403)', async () => { + const login = await request(httpServer) + .post('/auth/tenant/login') + .send({ email: tenantAAdminEmail, senha }) + .expect(200); + + await request(httpServer) + .get('/auth/master/admins') + .set('Authorization', `Bearer ${(login.body as TokenPair).access_token}`) + .expect(403); + }); + + it('token de master não acessa rota de tenant (403)', async () => { + const login = await request(httpServer) + .post('/auth/master/login') + .send({ email: masterEmail, senha }) + .expect(200); + + await request(httpServer) + .get('/auth/tenant/users') + .set('Authorization', `Bearer ${(login.body as TokenPair).access_token}`) + .expect(403); + }); + + it('token master acessa rota master normalmente', async () => { + const login = await request(httpServer) + .post('/auth/master/login') + .send({ email: masterEmail, senha }) + .expect(200); + + const res = await request(httpServer) + .get('/auth/master/admins') + .set('Authorization', `Bearer ${(login.body as TokenPair).access_token}`) + .expect(200); + + expect(Array.isArray(res.body)).toBe(true); + expect((res.body as Array<{ id: string }>).some((a) => a.id === masterId)).toBe(true); + }); + + it('vendedor tentando acessar rota restrita a tenant_admin é bloqueado pelo RolesGuard (403)', async () => { + const login = await request(httpServer) + .post('/auth/tenant/login') + .send({ email: tenantAVendedorEmail, senha }) + .expect(200); + + await request(httpServer) + .get('/auth/tenant/users') + .set('Authorization', `Bearer ${(login.body as TokenPair).access_token}`) + .expect(403); + }); + + it('tenant_admin acessa a lista de usuários do próprio tenant', async () => { + const login = await request(httpServer) + .post('/auth/tenant/login') + .send({ email: tenantAAdminEmail, senha }) + .expect(200); + + const res = await request(httpServer) + .get('/auth/tenant/users') + .set('Authorization', `Bearer ${(login.body as TokenPair).access_token}`) + .expect(200); + + const ids = (res.body as Array<{ id: string }>).map((u) => u.id); + expect(ids).toEqual(expect.arrayContaining([tenantAAdminId, tenantAVendedorId])); + }); + }); + + describe('isolamento entre tenants via RLS', () => { + it('dado de um tenant nunca vaza para outro, mesmo forçando tenant_id de outro tenant na query string', async () => { + const login = await request(httpServer) + .post('/auth/tenant/login') + .send({ email: tenantAAdminEmail, senha }) + .expect(200); + + const res = await request(httpServer) + .get(`/auth/tenant/users?tenant_id=${tenantBId}`) + .set('Authorization', `Bearer ${(login.body as TokenPair).access_token}`) + .expect(200); + + const ids = (res.body as Array<{ id: string }>).map((u) => u.id); + + // só usuários do tenant A (dono da sessão), nunca o do tenant B — + // mesmo pedindo explicitamente o tenant_id de B na query string. + expect(ids).toEqual(expect.arrayContaining([tenantAAdminId, tenantAVendedorId])); + expect(ids).not.toContain(tenantBAdminId); + }); + + it('login como tenant B só enxerga o próprio usuário, nunca os do tenant A', async () => { + const login = await request(httpServer) + .post('/auth/tenant/login') + .send({ email: tenantBAdminEmail, senha }) + .expect(200); + + const res = await request(httpServer) + .get('/auth/tenant/users') + .set('Authorization', `Bearer ${(login.body as TokenPair).access_token}`) + .expect(200); + + const ids = (res.body as Array<{ id: string }>).map((u) => u.id); + expect(ids).toEqual([tenantBAdminId]); + }); + }); + + describe('proteção contra força bruta', () => { + it('bloqueia com 429 após 5 tentativas de login falhas, mesmo se a senha correta vier em seguida', async () => { + for (let i = 0; i < 5; i += 1) { + await request(httpServer) + .post('/auth/tenant/login') + .send({ email: tenantBAdminEmail, senha: 'senha-errada' }) + .expect(401); + } + + // a 6ª tentativa é bloqueada por 429 mesmo com a senha correta + await request(httpServer) + .post('/auth/tenant/login') + .send({ email: tenantBAdminEmail, senha }) + .expect(429); + }); + }); +}); diff --git a/test/health.e2e-spec.ts b/test/health.e2e-spec.ts new file mode 100644 index 0000000..bcd5869 --- /dev/null +++ b/test/health.e2e-spec.ts @@ -0,0 +1,34 @@ +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import request, { Response } from 'supertest'; +import { AppModule } from '../src/app.module'; +import { HealthStatus } from '../src/modules/health/health.service'; + +describe('HealthController (e2e)', () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('GET /health retorna 200 com status da API e do banco', () => { + return request(app.getHttpServer() as Parameters[0]) + .get('/health') + .expect(200) + .expect((res: Response) => { + const body = res.body as HealthStatus; + expect(body.status).toBeDefined(); + expect(body.database).toBeDefined(); + expect(body.timestamp).toBeDefined(); + }); + }); +}); diff --git a/test/integracoes.e2e-spec.ts b/test/integracoes.e2e-spec.ts new file mode 100644 index 0000000..425a161 --- /dev/null +++ b/test/integracoes.e2e-spec.ts @@ -0,0 +1,486 @@ +import { randomUUID } from 'node:crypto'; +import { IncomingMessage, Server, ServerResponse, createServer } from 'node:http'; +import { AddressInfo } from 'node:net'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; +import { EncryptionService } from '../src/common/encryption.service'; +import { PrismaService } from '../src/config/prisma.service'; +import { hashPassword } from '../src/modules/auth/password.util'; + +// Nome com prefixo "mock" para escapar da restrição do Jest contra +// variáveis fora de escopo dentro da factory de `jest.mock`. +const mockSendMail = jest.fn().mockResolvedValue({ messageId: 'fake-message-id' }); +jest.mock('nodemailer', () => ({ + createTransport: jest.fn(() => ({ sendMail: mockSendMail })), +})); + +interface TokenPair { + access_token: string; + refresh_token: string; +} + +/** + * Substituto local da Evolution API real (indisponível neste ambiente de + * teste/CI — sem internet nem instância dedicada). Implementa o subset da + * API v2 usado por `EvolutionApiClient`, o suficiente para exercitar o + * fluxo ponta a ponta desta etapa contra um servidor HTTP real. + */ +class FakeEvolutionServer { + server: Server; + baseUrl = ''; + connectionState: 'open' | 'connecting' | 'close' = 'close'; + requests: Array<{ method: string; url: string; apikey?: string; body: unknown }> = []; + + async start(): Promise { + this.server = createServer((req, res) => this.handle(req, res)); + await new Promise((resolve) => this.server.listen(0, '127.0.0.1', resolve)); + const { port } = this.server.address() as AddressInfo; + this.baseUrl = `http://127.0.0.1:${port}`; + } + + async stop(): Promise { + await new Promise((resolve) => this.server.close(() => resolve())); + } + + private handle(req: IncomingMessage, res: ServerResponse): void { + const chunks: Buffer[] = []; + req.on('data', (chunk: Buffer) => chunks.push(chunk)); + req.on('end', () => { + const raw = Buffer.concat(chunks).toString('utf8'); + const body: unknown = raw ? JSON.parse(raw) : undefined; + const url = req.url ?? ''; + const method = req.method ?? 'GET'; + this.requests.push({ method, url, apikey: req.headers.apikey as string | undefined, body }); + + const json = (status: number, payload: unknown) => { + res.writeHead(status, { 'content-type': 'application/json' }); + res.end(JSON.stringify(payload)); + }; + + if (method === 'GET' && url === '/') { + return json(200, { version: '2.3.0', message: 'Welcome to the Evolution API' }); + } + if (method === 'POST' && url === '/instance/create') { + return json(201, { instance: body }); + } + if (method === 'GET' && url.startsWith('/instance/connect/')) { + return json(200, { base64: 'data:image/png;base64,ZmFrZQ==', pairingCode: '12-34' }); + } + if (method === 'GET' && url.startsWith('/instance/connectionState/')) { + return json(200, { instance: { state: this.connectionState } }); + } + if (method === 'DELETE' && url.startsWith('/instance/logout/')) { + return json(200, {}); + } + if (method === 'DELETE' && url.startsWith('/instance/delete/')) { + return json(200, {}); + } + if (method === 'POST' && url.startsWith('/message/sendText/')) { + const numero = (body as { number?: string } | undefined)?.number; + if (numero === '0000000000') { + return json(500, { message: 'falha simulada de envio' }); + } + return json(201, { status: 'PENDING' }); + } + + json(404, { message: 'rota não simulada' }); + }); + } +} + +describe('Integrações — Evolution API (WhatsApp) e E-mail (e2e)', () => { + let app: INestApplication; + let prisma: PrismaService; + let encryptionService: EncryptionService; + let httpServer: Parameters[0]; + let evolution: FakeEvolutionServer; + + const senha = 'SenhaForteDemo#26'; + const suffix = randomUUID().slice(0, 8); + const masterEmail = `master-etapa4-${suffix}@teste.com`; + + let masterId: string; + let masterToken: string; + + const tenantIds: string[] = []; + const planoIds: string[] = []; + const tenantUserIds: string[] = []; + const evolutionConfigIds: string[] = []; + const masterEmailConfigIds: string[] = []; + + async function authMaster(): Promise { + const res = await request(httpServer) + .post('/auth/master/login') + .send({ email: masterEmail, senha }) + .expect(200); + return (res.body as TokenPair).access_token; + } + + function asMaster() { + return { + get: (url: string) => + request(httpServer).get(url).set('Authorization', `Bearer ${masterToken}`), + post: (url: string) => + request(httpServer).post(url).set('Authorization', `Bearer ${masterToken}`), + put: (url: string) => + request(httpServer).put(url).set('Authorization', `Bearer ${masterToken}`), + delete: (url: string) => + request(httpServer).delete(url).set('Authorization', `Bearer ${masterToken}`), + }; + } + + function asTenant(token: string) { + return { + get: (url: string) => request(httpServer).get(url).set('Authorization', `Bearer ${token}`), + post: (url: string) => request(httpServer).post(url).set('Authorization', `Bearer ${token}`), + put: (url: string) => request(httpServer).put(url).set('Authorization', `Bearer ${token}`), + delete: (url: string) => + request(httpServer).delete(url).set('Authorization', `Bearer ${token}`), + }; + } + + async function criarPlano(overrides: Partial> = {}) { + const res = await asMaster() + .post('/master/planos') + .send({ + nome: `Plano ${suffix} ${randomUUID().slice(0, 4)}`, + valor_mensal: 199.9, + limite_instancias_whatsapp: 1, + ...overrides, + }) + .expect(201); + planoIds.push((res.body as { id: string }).id); + return res.body as { id: string }; + } + + async function criarTenantComAdmin(planoId: string) { + const emailAdmin = `admin-${randomUUID().slice(0, 8)}-${suffix}@teste.com`; + const res = await asMaster() + .post('/master/tenants') + .send({ + razao_social: `Tenant Integração ${suffix} ${randomUUID().slice(0, 6)}`, + email_contato: `contato-${randomUUID().slice(0, 6)}@teste.com`, + plano_id: planoId, + admin: { nome: 'Admin de Teste', email: emailAdmin }, + }) + .expect(201); + const body = res.body as { + tenant: { id: string }; + admin: { id: string; email: string; senha_provisoria: string }; + }; + tenantIds.push(body.tenant.id); + tenantUserIds.push(body.admin.id); + + await prisma.tenants.update({ where: { id: body.tenant.id }, data: { status: 'ativo' } }); + + const login = await request(httpServer) + .post('/auth/tenant/login') + .send({ email: body.admin.email, senha: body.admin.senha_provisoria }) + .expect(200); + + return { + tenant: body.tenant, + admin: body.admin, + token: (login.body as TokenPair).access_token, + }; + } + + async function criarEvolutionConfigAtiva() { + const res = await asMaster() + .post('/master/evolution-config') + .send({ + nome: `Evolution ${suffix}`, + base_url: evolution.baseUrl, + api_key: 'chave-evolution-secreta-1234', + }) + .expect(201); + const body = res.body as { id: string }; + evolutionConfigIds.push(body.id); + return body; + } + + beforeAll(async () => { + evolution = new FakeEvolutionServer(); + await evolution.start(); + + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }), + ); + await app.init(); + httpServer = app.getHttpServer() as Parameters[0]; + + prisma = app.get(PrismaService); + encryptionService = app.get(EncryptionService); + + const senha_hash = await hashPassword(senha); + const master = await prisma.master_admins.create({ + data: { nome: 'Master Etapa 4', email: masterEmail, senha_hash }, + }); + masterId = master.id; + + masterToken = await authMaster(); + }); + + afterAll(async () => { + await prisma.tenant_whatsapp_instances.deleteMany({ where: { tenant_id: { in: tenantIds } } }); + await prisma.tenant_email_config.deleteMany({ where: { tenant_id: { in: tenantIds } } }); + await prisma.tenants.deleteMany({ where: { id: { in: tenantIds } } }); + await prisma.planos.deleteMany({ where: { id: { in: planoIds } } }); + await prisma.evolution_api_config.deleteMany({ where: { id: { in: evolutionConfigIds } } }); + await prisma.master_email_config.deleteMany({ where: { id: { in: masterEmailConfigIds } } }); + await prisma.auth_refresh_tokens.deleteMany({ + where: { usuario_id: { in: [masterId, ...tenantUserIds] } }, + }); + await prisma.login_attempts.deleteMany({ + where: { usuario_tipo: 'master', email: masterEmail }, + }); + await prisma.audit_logs.deleteMany({ where: { usuario_id: masterId } }); + await prisma.master_admins.deleteMany({ where: { id: masterId } }); + + await app.close(); + await evolution.stop(); + }); + + describe('Evolution API — config master', () => { + it('cadastra config, nunca retorna api_key em texto puro e testa conexão', async () => { + const config = await criarEvolutionConfigAtiva(); + const body = config as unknown as Record; + + expect(body.api_key_cripto).toBeUndefined(); + expect(body.api_key).toMatch(/^\*+1234$/); + + const raw = await prisma.evolution_api_config.findUniqueOrThrow({ where: { id: config.id } }); + expect(raw.api_key_cripto).not.toBe('chave-evolution-secreta-1234'); + expect(encryptionService.decrypt(raw.api_key_cripto)).toBe('chave-evolution-secreta-1234'); + + const lista = await asMaster().get('/master/evolution-config').expect(200); + expect( + (lista.body as Array>).every((c) => c.api_key_cripto === undefined), + ).toBe(true); + + const teste = await asMaster() + .post(`/master/evolution-config/${config.id}/testar-conexao`) + .expect(201); + expect((teste.body as { status_http: number; versao: string }).status_http).toBe(200); + expect((teste.body as { versao: string }).versao).toBe('2.3.0'); + }); + + it('bloqueia endpoints de evolution-config com token de tenant_user', async () => { + const plano = await criarPlano(); + const { token } = await criarTenantComAdmin(plano.id); + + await asTenant(token).get('/master/evolution-config').expect(403); + }); + }); + + describe('WhatsApp — instâncias do tenant', () => { + it('cria instância, obtém QR code e reflete status conectado após consultar', async () => { + await criarEvolutionConfigAtiva(); + const plano = await criarPlano({ limite_instancias_whatsapp: 1 }); + const { token } = await criarTenantComAdmin(plano.id); + + const criada = await asTenant(token).post('/tenant/whatsapp/instancias').send({}).expect(201); + const instancia = criada.body as { id: string; status: string; instance_name: string }; + expect(instancia.status).toBe('desconectado'); + expect(instancia.instance_name).toMatch(/^tenant-/); + + const qrcode = await asTenant(token) + .get(`/tenant/whatsapp/instancias/${instancia.id}/qrcode`) + .expect(200); + expect((qrcode.body as { base64: string }).base64).toContain('base64'); + + evolution.connectionState = 'open'; + const status = await asTenant(token) + .get(`/tenant/whatsapp/instancias/${instancia.id}/status`) + .expect(200); + expect((status.body as { status: string }).status).toBe('conectado'); + }); + + it('criar instância além do limite do plano retorna erro claro', async () => { + await criarEvolutionConfigAtiva(); + const plano = await criarPlano({ limite_instancias_whatsapp: 1 }); + const { token } = await criarTenantComAdmin(plano.id); + + await asTenant(token).post('/tenant/whatsapp/instancias').send({}).expect(201); + const segunda = await asTenant(token) + .post('/tenant/whatsapp/instancias') + .send({}) + .expect(400); + expect((segunda.body as { message: string }).message).toMatch(/limite/i); + }); + + it('teste de envio funciona (sucesso) e reporta falha claramente quando a Evolution API recusa', async () => { + await criarEvolutionConfigAtiva(); + const plano = await criarPlano({ limite_instancias_whatsapp: 1 }); + const { token } = await criarTenantComAdmin(plano.id); + + const criada = await asTenant(token).post('/tenant/whatsapp/instancias').send({}).expect(201); + const instanceId = (criada.body as { id: string }).id; + + const sucesso = await asTenant(token) + .post(`/tenant/whatsapp/instancias/${instanceId}/testar-envio`) + .send({ numero: '5511999998888' }) + .expect(201); + expect(sucesso.body).toMatchObject({ sucesso: true }); + + const falha = await asTenant(token) + .post(`/tenant/whatsapp/instancias/${instanceId}/testar-envio`) + .send({ numero: '0000000000' }) + .expect(201); + expect((falha.body as { sucesso: boolean; erro: string }).sucesso).toBe(false); + expect((falha.body as { erro: string }).erro).toEqual(expect.any(String)); + }); + + it('desconecta e remove a instância', async () => { + await criarEvolutionConfigAtiva(); + const plano = await criarPlano({ limite_instancias_whatsapp: 1 }); + const { token } = await criarTenantComAdmin(plano.id); + + const criada = await asTenant(token).post('/tenant/whatsapp/instancias').send({}).expect(201); + const instanceId = (criada.body as { id: string }).id; + + await asTenant(token).delete(`/tenant/whatsapp/instancias/${instanceId}`).expect(204); + await asTenant(token).get(`/tenant/whatsapp/instancias/${instanceId}/status`).expect(404); + }); + + it('bloqueia acesso a instância de outro tenant (RLS) com 404', async () => { + await criarEvolutionConfigAtiva(); + const planoA = await criarPlano({ limite_instancias_whatsapp: 1 }); + const { token: tokenA } = await criarTenantComAdmin(planoA.id); + const planoB = await criarPlano({ limite_instancias_whatsapp: 1 }); + const { token: tokenB } = await criarTenantComAdmin(planoB.id); + + const criada = await asTenant(tokenA) + .post('/tenant/whatsapp/instancias') + .send({}) + .expect(201); + const instanceId = (criada.body as { id: string }).id; + + await asTenant(tokenB).get(`/tenant/whatsapp/instancias/${instanceId}/status`).expect(404); + }); + }); + + describe('E-mail — config master e tenant', () => { + async function criarMasterEmailConfig(overrides: Partial> = {}) { + const res = await asMaster() + .post('/master/email-config') + .send({ + smtp_host: 'smtp.padrao.teste', + smtp_porta: 587, + smtp_usuario: 'padrao@teste.com', + smtp_senha: 'senha-smtp-padrao-9876', + email_remetente: 'padrao@seconectai.teste', + ...overrides, + }) + .expect(201); + const body = res.body as { id: string }; + masterEmailConfigIds.push(body.id); + return body; + } + + it('cadastra config padrão da plataforma sem nunca retornar a senha em texto puro', async () => { + const config = await criarMasterEmailConfig(); + const body = config as unknown as Record; + + expect(body.smtp_senha_cripto).toBeUndefined(); + expect(body.smtp_senha).toMatch(/^\*+9876$/); + + const raw = await prisma.master_email_config.findUniqueOrThrow({ where: { id: config.id } }); + expect(encryptionService.decrypt(raw.smtp_senha_cripto)).toBe('senha-smtp-padrao-9876'); + }); + + it('tenant usa o padrão por default e consegue trocar para SMTP próprio (PUT exige campos)', async () => { + await criarMasterEmailConfig(); + const plano = await criarPlano(); + const { token } = await criarTenantComAdmin(plano.id); + + const inicial = await asTenant(token).get('/tenant/email-config').expect(200); + expect(inicial.body).toEqual({ usar_config_padrao: true }); + + await asTenant(token) + .put('/tenant/email-config') + .send({ usar_config_padrao: false }) + .expect(400); + + const proprio = await asTenant(token) + .put('/tenant/email-config') + .send({ + usar_config_padrao: false, + smtp_host: 'smtp.proprio.teste', + smtp_porta: 465, + smtp_usuario: 'tenant@proprio.teste', + smtp_senha: 'senha-smtp-propria-4321', + email_remetente: 'contato@tenantproprio.teste', + }) + .expect(200); + expect(proprio.body).toMatchObject({ + usar_config_padrao: false, + smtp_host: 'smtp.proprio.teste', + email_remetente: 'contato@tenantproprio.teste', + }); + expect((proprio.body as { smtp_senha: string }).smtp_senha).toMatch(/^\*+4321$/); + + const voltaPadrao = await asTenant(token) + .put('/tenant/email-config') + .send({ usar_config_padrao: true }) + .expect(200); + expect(voltaPadrao.body).toEqual({ usar_config_padrao: true }); + }); + + it('teste de envio funciona usando o padrão e usando o SMTP próprio do tenant', async () => { + await criarMasterEmailConfig({ email_remetente: `padrao-${suffix}@seconectai.teste` }); + const plano = await criarPlano(); + const { token } = await criarTenantComAdmin(plano.id); + + mockSendMail.mockClear(); + await asTenant(token) + .post('/tenant/email-config/testar-envio') + .send({ destinatario: 'destino@teste.com' }) + .expect(201) + .expect((res) => { + if (!(res.body as { sucesso: boolean }).sucesso) { + throw new Error(`envio via padrão falhou: ${JSON.stringify(res.body)}`); + } + }); + expect(mockSendMail).toHaveBeenCalledWith( + expect.objectContaining({ from: `padrao-${suffix}@seconectai.teste` }), + ); + + await asTenant(token) + .put('/tenant/email-config') + .send({ + usar_config_padrao: false, + smtp_host: 'smtp.proprio.teste', + smtp_porta: 465, + smtp_usuario: 'tenant@proprio.teste', + smtp_senha: 'senha-smtp-propria-4321', + email_remetente: `proprio-${suffix}@tenantproprio.teste`, + }) + .expect(200); + + mockSendMail.mockClear(); + await asTenant(token) + .post('/tenant/email-config/testar-envio') + .send({ destinatario: 'destino@teste.com' }) + .expect(201); + expect(mockSendMail).toHaveBeenCalledWith( + expect.objectContaining({ from: `proprio-${suffix}@tenantproprio.teste` }), + ); + }); + + it('bloqueia endpoints de email-config do tenant com token de master e vice-versa', async () => { + const plano = await criarPlano(); + const { token } = await criarTenantComAdmin(plano.id); + + await asTenant(token).get('/master/email-config').expect(403); + await asMaster().get('/tenant/email-config').expect(403); + }); + }); +}); diff --git a/test/jest-e2e.json b/test/jest-e2e.json new file mode 100644 index 0000000..e9d912f --- /dev/null +++ b/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/test/master.e2e-spec.ts b/test/master.e2e-spec.ts new file mode 100644 index 0000000..27200e2 --- /dev/null +++ b/test/master.e2e-spec.ts @@ -0,0 +1,551 @@ +import { randomUUID } from 'node:crypto'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; +import { PrismaService } from '../src/config/prisma.service'; +import { hashPassword } from '../src/modules/auth/password.util'; + +interface TokenPair { + access_token: string; + refresh_token: string; +} + +function referenciaAtual(): string { + const agora = new Date(); + return `${agora.getUTCFullYear()}-${String(agora.getUTCMonth() + 1).padStart(2, '0')}`; +} + +describe('Master — Planos, Tenants e Faturas (e2e)', () => { + let app: INestApplication; + let prisma: PrismaService; + let httpServer: Parameters[0]; + + const senha = 'SenhaForteDemo#26'; + const suffix = randomUUID().slice(0, 8); + const masterEmail = `master-etapa3-${suffix}@teste.com`; + + let masterId: string; + let masterToken: string; + + const tenantIds: string[] = []; + const planoIds: string[] = []; + const tenantUserIds: string[] = []; + + async function authMaster(): Promise { + const res = await request(httpServer) + .post('/auth/master/login') + .send({ email: masterEmail, senha }) + .expect(200); + return (res.body as TokenPair).access_token; + } + + function asMaster() { + return { + get: (url: string) => + request(httpServer).get(url).set('Authorization', `Bearer ${masterToken}`), + post: (url: string) => + request(httpServer).post(url).set('Authorization', `Bearer ${masterToken}`), + put: (url: string) => + request(httpServer).put(url).set('Authorization', `Bearer ${masterToken}`), + patch: (url: string) => + request(httpServer).patch(url).set('Authorization', `Bearer ${masterToken}`), + delete: (url: string) => + request(httpServer).delete(url).set('Authorization', `Bearer ${masterToken}`), + }; + } + + async function criarPlano(overrides: Partial> = {}) { + const res = await asMaster() + .post('/master/planos') + .send({ + nome: `Plano ${suffix} ${randomUUID().slice(0, 4)}`, + valor_mensal: 199.9, + limite_customers: 100, + limite_usuarios: 5, + limite_instancias_whatsapp: 1, + ...overrides, + }) + .expect(201); + planoIds.push((res.body as { id: string }).id); + return res.body as { id: string; valor_mensal: string; ativo: boolean }; + } + + async function criarTenant(planoId: string, overrides: Partial> = {}) { + const emailAdmin = `admin-${randomUUID().slice(0, 8)}-${suffix}@teste.com`; + const res = await asMaster() + .post('/master/tenants') + .send({ + razao_social: `Tenant ${suffix} ${randomUUID().slice(0, 6)}`, + email_contato: `contato-${randomUUID().slice(0, 6)}@teste.com`, + plano_id: planoId, + admin: { nome: 'Admin de Teste', email: emailAdmin }, + ...overrides, + }) + .expect(201); + const body = res.body as { + tenant: { id: string }; + admin: { id: string; email: string; senha_provisoria: string }; + }; + tenantIds.push(body.tenant.id); + tenantUserIds.push(body.admin.id); + return body; + } + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }), + ); + await app.init(); + httpServer = app.getHttpServer() as Parameters[0]; + + prisma = app.get(PrismaService); + + const senha_hash = await hashPassword(senha); + const master = await prisma.master_admins.create({ + data: { nome: 'Master Etapa 3', email: masterEmail, senha_hash }, + }); + masterId = master.id; + + masterToken = await authMaster(); + }); + + afterAll(async () => { + // tenants em cascata levam tenant_users e tenant_faturas junto + // (onDelete: Cascade no schema); planos só depois, pois tenants tem FK + // pra planos. + await prisma.tenants.deleteMany({ where: { id: { in: tenantIds } } }); + await prisma.planos.deleteMany({ where: { id: { in: planoIds } } }); + await prisma.auth_refresh_tokens.deleteMany({ + where: { usuario_id: { in: [masterId, ...tenantUserIds] } }, + }); + await prisma.login_attempts.deleteMany({ + where: { usuario_tipo: 'master', email: masterEmail }, + }); + await prisma.audit_logs.deleteMany({ where: { usuario_id: masterId } }); + await prisma.master_admins.deleteMany({ where: { id: masterId } }); + + await app.close(); + }); + + describe('CRUD de planos', () => { + it('cria plano válido', async () => { + const plano = await criarPlano({ valor_mensal: 249.5 }); + expect(plano.valor_mensal).toBe('249.5'); + expect(plano.ativo).toBe(true); + }); + + it('rejeita valor_mensal <= 0', async () => { + await asMaster() + .post('/master/planos') + .send({ nome: 'Plano inválido', valor_mensal: 0 }) + .expect(400); + }); + + it('rejeita limite <= 0 quando informado', async () => { + await asMaster() + .post('/master/planos') + .send({ nome: 'Plano inválido', valor_mensal: 10, limite_customers: 0 }) + .expect(400); + }); + + it('lista, detalha e atualiza', async () => { + const plano = await criarPlano(); + + const lista = await asMaster().get('/master/planos').expect(200); + expect((lista.body as Array<{ id: string }>).some((p) => p.id === plano.id)).toBe(true); + + const detalhe = await asMaster().get(`/master/planos/${plano.id}`).expect(200); + expect((detalhe.body as { id: string }).id).toBe(plano.id); + + const atualizado = await asMaster() + .put(`/master/planos/${plano.id}`) + .send({ nome: 'Plano Atualizado', valor_mensal: 299.9 }) + .expect(200); + expect((atualizado.body as { nome: string }).nome).toBe('Plano Atualizado'); + }); + + it('exclui plano não usado por nenhum tenant', async () => { + const plano = await criarPlano(); + + const res = await asMaster().delete(`/master/planos/${plano.id}`).expect(200); + expect(res.body).toMatchObject({ deletado: true, desativado: false }); + + await asMaster().get(`/master/planos/${plano.id}`).expect(404); + // já foi removido do array de cleanup implicitamente — não afeta o afterAll + // (deleteMany ignora ids que não existem mais). + }); + + it('não permite excluir plano em uso por tenant ativo — desativa em vez disso', async () => { + const plano = await criarPlano(); + await criarTenant(plano.id); + + const res = await asMaster().delete(`/master/planos/${plano.id}`).expect(200); + expect(res.body).toMatchObject({ deletado: false, desativado: true }); + + const detalhe = await asMaster().get(`/master/planos/${plano.id}`).expect(200); + expect((detalhe.body as { ativo: boolean }).ativo).toBe(false); + }); + + it('todos os endpoints de planos são bloqueados com token de tenant_user', async () => { + const plano = await criarPlano(); + const { admin } = await criarTenant(plano.id); + + const loginTenant = await request(httpServer) + .post('/auth/tenant/login') + .send({ email: admin.email, senha: admin.senha_provisoria }) + .expect(200); + const tenantToken = (loginTenant.body as TokenPair).access_token; + + await request(httpServer) + .get('/master/planos') + .set('Authorization', `Bearer ${tenantToken}`) + .expect(403); + }); + }); + + describe('Tenants', () => { + it('cria tenant com o primeiro tenant_admin funcional (login testado) e grava auditoria', async () => { + const plano = await criarPlano(); + const { tenant, admin } = await criarTenant(plano.id); + + expect(tenant).toMatchObject({ status: 'trial' }); + expect(admin.senha_provisoria).toEqual(expect.any(String)); + expect(admin.senha_provisoria.length).toBeGreaterThanOrEqual(10); + + const login = await request(httpServer) + .post('/auth/tenant/login') + .send({ email: admin.email, senha: admin.senha_provisoria }) + .expect(200); + expect((login.body as TokenPair).access_token).toEqual(expect.any(String)); + + const log = await prisma.audit_logs.findFirst({ + where: { acao: 'criar_tenant', entidade_id: tenant.id }, + }); + expect(log).not.toBeNull(); + expect(log?.usuario_id).toBe(masterId); + }); + + it('rejeita criação com plano inexistente', async () => { + await asMaster() + .post('/master/tenants') + .send({ + razao_social: 'Tenant Plano Inválido', + email_contato: `x-${randomUUID()}@teste.com`, + plano_id: randomUUID(), + admin: { nome: 'Admin', email: `y-${randomUUID()}@teste.com` }, + }) + .expect(400); + }); + + it('listagem com filtro por status e busca funciona', async () => { + const plano = await criarPlano(); + const marcador = `buscamarcador${suffix}`; + const { tenant } = await criarTenant(plano.id, { razao_social: `Empresa ${marcador} Ltda` }); + + const porBusca = await asMaster().get(`/master/tenants?busca=${marcador}`).expect(200); + const idsBusca = (porBusca.body as { data: Array<{ id: string }> }).data.map((t) => t.id); + expect(idsBusca).toEqual([tenant.id]); + + const porStatus = await asMaster() + .get(`/master/tenants?busca=${marcador}&status=trial`) + .expect(200); + expect((porStatus.body as { data: Array<{ id: string }> }).data.map((t) => t.id)).toEqual([ + tenant.id, + ]); + + const statusErrado = await asMaster() + .get(`/master/tenants?busca=${marcador}&status=cancelado`) + .expect(200); + expect((statusErrado.body as { data: unknown[] }).data).toEqual([]); + }); + + it('detalhe do tenant traz plano, faturas e contadores agregados', async () => { + const plano = await criarPlano(); + const { tenant } = await criarTenant(plano.id); + + await asMaster().post('/master/faturas/gerar-mes').send({}).expect(200); + + const detalhe = await asMaster().get(`/master/tenants/${tenant.id}`).expect(200); + const body = detalhe.body as { + plano: { id: string }; + faturas: unknown[]; + _count: { usuarios: number; customers: number; vendas: number }; + }; + expect(body.plano.id).toBe(plano.id); + expect(body._count).toMatchObject({ usuarios: 1, customers: 0, vendas: 0 }); + // o tenant começa como `trial`; gerar-mes só gera fatura pra quem já + // está `ativo`, então aqui a lista de faturas fica vazia mesmo. + expect(Array.isArray(body.faturas)).toBe(true); + }); + + it('atualiza dados cadastrais via PUT', async () => { + const plano = await criarPlano(); + const { tenant } = await criarTenant(plano.id); + + const atualizado = await asMaster() + .put(`/master/tenants/${tenant.id}`) + .send({ telefone: '11999998888' }) + .expect(200); + expect((atualizado.body as { telefone: string }).telefone).toBe('11999998888'); + }); + + it('muda status para suspenso: bloqueia login novo e revoga acesso de token já emitido (TenantGuard) + audita', async () => { + const plano = await criarPlano(); + const { tenant, admin } = await criarTenant(plano.id); + + const login = await request(httpServer) + .post('/auth/tenant/login') + .send({ email: admin.email, senha: admin.senha_provisoria }) + .expect(200); + const tenantToken = (login.body as TokenPair).access_token; + + // antes de suspender, o token funciona normalmente numa rota com TenantGuard + await request(httpServer) + .get('/auth/tenant/users') + .set('Authorization', `Bearer ${tenantToken}`) + .expect(200); + + await asMaster() + .patch(`/master/tenants/${tenant.id}/status`) + .send({ status: 'suspenso' }) + .expect(200); + + // login novo passa a ser rejeitado + await request(httpServer) + .post('/auth/tenant/login') + .send({ email: admin.email, senha: admin.senha_provisoria }) + .expect(401); + + // o token de acesso emitido antes da suspensão, ainda não expirado, + // é bloqueado pelo TenantGuard (checa o status do tenant a cada request) + await request(httpServer) + .get('/auth/tenant/users') + .set('Authorization', `Bearer ${tenantToken}`) + .expect(403); + + const log = await prisma.audit_logs.findFirst({ + where: { acao: 'mudar_status_tenant', entidade_id: tenant.id }, + }); + expect(log?.detalhes).toMatchObject({ de: 'trial', para: 'suspenso' }); + }); + + it('muda status para cancelado também bloqueia o acesso', async () => { + const plano = await criarPlano(); + const { tenant, admin } = await criarTenant(plano.id); + + await asMaster() + .patch(`/master/tenants/${tenant.id}/status`) + .send({ status: 'cancelado' }) + .expect(200); + + await request(httpServer) + .post('/auth/tenant/login') + .send({ email: admin.email, senha: admin.senha_provisoria }) + .expect(401); + }); + + it('todos os endpoints de tenants são bloqueados com token de tenant_user', async () => { + const plano = await criarPlano(); + const { admin } = await criarTenant(plano.id); + + const loginTenant = await request(httpServer) + .post('/auth/tenant/login') + .send({ email: admin.email, senha: admin.senha_provisoria }) + .expect(200); + const tenantToken = (loginTenant.body as TokenPair).access_token; + + await request(httpServer) + .get('/master/tenants') + .set('Authorization', `Bearer ${tenantToken}`) + .expect(403); + }); + }); + + describe('Faturas', () => { + it('gerar-mes não duplica fatura já existente para o mesmo tenant/referência', async () => { + const plano = await criarPlano({ valor_mensal: 150 }); + const { tenant } = await criarTenant(plano.id); + await prisma.tenants.update({ where: { id: tenant.id }, data: { status: 'ativo' } }); + + const referencia = referenciaAtual(); + + await asMaster().post('/master/faturas/gerar-mes').send({ referencia }).expect(200); + await asMaster().post('/master/faturas/gerar-mes').send({ referencia }).expect(200); + + const faturas = await prisma.tenant_faturas.findMany({ + where: { tenant_id: tenant.id, referencia }, + }); + expect(faturas).toHaveLength(1); + expect(faturas[0].valor.toString()).toBe('150'); + }); + + it('marcar fatura como paga reverte tenant inadimplente -> ativo e audita', async () => { + const plano = await criarPlano(); + const { tenant } = await criarTenant(plano.id); + await prisma.tenants.update({ where: { id: tenant.id }, data: { status: 'inadimplente' } }); + + const fatura = await prisma.tenant_faturas.create({ + data: { + tenant_id: tenant.id, + referencia: '2020-01', + valor: 100, + vencimento: new Date('2020-01-10T00:00:00Z'), + }, + }); + + const res = await asMaster().patch(`/master/faturas/${fatura.id}/marcar-pago`).expect(200); + expect((res.body as { status: string; pago_em: string }).status).toBe('pago'); + + const tenantAtualizado = await prisma.tenants.findUnique({ where: { id: tenant.id } }); + expect(tenantAtualizado?.status).toBe('ativo'); + + // idempotência: marcar de novo uma fatura já paga é rejeitado + await asMaster().patch(`/master/faturas/${fatura.id}/marcar-pago`).expect(409); + + const log = await prisma.audit_logs.findFirst({ + where: { acao: 'marcar_fatura_paga', entidade_id: fatura.id }, + }); + expect(log).not.toBeNull(); + }); + + it('atualizar-atrasadas marca fatura vencida como atrasada e tenant como inadimplente', async () => { + const plano = await criarPlano(); + const { tenant } = await criarTenant(plano.id); + await prisma.tenants.update({ where: { id: tenant.id }, data: { status: 'ativo' } }); + + const fatura = await prisma.tenant_faturas.create({ + data: { + tenant_id: tenant.id, + referencia: '2020-02', + valor: 100, + vencimento: new Date('2020-02-10T00:00:00Z'), + }, + }); + + await asMaster().post('/master/faturas/atualizar-atrasadas').send({}).expect(200); + + const faturaAtualizada = await prisma.tenant_faturas.findUnique({ where: { id: fatura.id } }); + expect(faturaAtualizada?.status).toBe('atrasado'); + + const tenantAtualizado = await prisma.tenants.findUnique({ where: { id: tenant.id } }); + expect(tenantAtualizado?.status).toBe('inadimplente'); + + const log = await prisma.audit_logs.findFirst({ + where: { acao: 'atualizar_faturas_atrasadas' }, + orderBy: { criado_em: 'desc' }, + }); + expect((log?.detalhes as { tenant_ids: string[] })?.tenant_ids).toContain(tenant.id); + }); + + it('listagem de faturas filtra por tenant e status', async () => { + const plano = await criarPlano(); + const { tenant } = await criarTenant(plano.id); + await prisma.tenant_faturas.create({ + data: { + tenant_id: tenant.id, + referencia: '2020-03', + valor: 50, + vencimento: new Date('2020-03-10'), + }, + }); + + const res = await asMaster() + .get(`/master/faturas?tenant_id=${tenant.id}&status=pendente`) + .expect(200); + const data = (res.body as { data: Array<{ tenant_id: string; status: string }> }).data; + expect(data.length).toBeGreaterThanOrEqual(1); + expect(data.every((f) => f.tenant_id === tenant.id && f.status === 'pendente')).toBe(true); + }); + + it('todos os endpoints de faturas são bloqueados com token de tenant_user', async () => { + const plano = await criarPlano(); + const { admin } = await criarTenant(plano.id); + + const loginTenant = await request(httpServer) + .post('/auth/tenant/login') + .send({ email: admin.email, senha: admin.senha_provisoria }) + .expect(200); + const tenantToken = (loginTenant.body as TokenPair).access_token; + + await request(httpServer) + .get('/master/faturas') + .set('Authorization', `Bearer ${tenantToken}`) + .expect(403); + }); + }); + + describe('Dashboard', () => { + it('retorna números corretos (conferidos por delta antes/depois)', async () => { + const antes = await asMaster().get('/master/dashboard').expect(200); + const bodyAntes = antes.body as { + tenants_por_status: Record; + mrr: string; + total_recebido_mes: string; + total_em_aberto: string; + }; + + const plano = await criarPlano({ valor_mensal: 321.5 }); + const { tenant } = await criarTenant(plano.id); + await prisma.tenants.update({ where: { id: tenant.id }, data: { status: 'ativo' } }); + + await prisma.tenant_faturas.create({ + data: { + tenant_id: tenant.id, + referencia: '2020-04', + valor: 77.25, + status: 'pago', + pago_em: new Date(), + vencimento: new Date('2020-04-10'), + }, + }); + await prisma.tenant_faturas.create({ + data: { + tenant_id: tenant.id, + referencia: '2020-05', + valor: 33.1, + status: 'pendente', + vencimento: new Date('2020-05-10'), + }, + }); + + const depois = await asMaster().get('/master/dashboard').expect(200); + const bodyDepois = depois.body as typeof bodyAntes; + + expect(bodyDepois.tenants_por_status.ativo).toBeGreaterThanOrEqual( + (bodyAntes.tenants_por_status.ativo ?? 0) + 1, + ); + + const deltaMrr = Number(bodyDepois.mrr) - Number(bodyAntes.mrr); + expect(deltaMrr).toBeCloseTo(321.5, 2); + + const deltaRecebido = + Number(bodyDepois.total_recebido_mes) - Number(bodyAntes.total_recebido_mes); + expect(deltaRecebido).toBeCloseTo(77.25, 2); + + const deltaAberto = Number(bodyDepois.total_em_aberto) - Number(bodyAntes.total_em_aberto); + expect(deltaAberto).toBeCloseTo(33.1, 2); + }); + + it('bloqueado com token de tenant_user', async () => { + const plano = await criarPlano(); + const { admin } = await criarTenant(plano.id); + + const loginTenant = await request(httpServer) + .post('/auth/tenant/login') + .send({ email: admin.email, senha: admin.senha_provisoria }) + .expect(200); + const tenantToken = (loginTenant.body as TokenPair).access_token; + + await request(httpServer) + .get('/master/dashboard') + .set('Authorization', `Bearer ${tenantToken}`) + .expect(403); + }); + }); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a3fd2c5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true + } +}