From 62b009f8a2e199e96fccf6a3ae6d892cdaf56990 Mon Sep 17 00:00:00 2001 From: BenevanioSantos Date: Wed, 7 Oct 2026 12:11:31 -0300 Subject: [PATCH 1/2] =?UTF-8?q?feat(PAV-177):=20otimiza=20gera=C3=A7=C3=A3?= =?UTF-8?q?o=20de=20curr=C3=ADculo=20para=20ATS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 7 + Dockerfile | 19 + README.md | 65 +- package-lock.json | 4000 ++++++++++++++++- package.json | 29 +- .../dtos/GenerateFromProfileInput.ts | 14 + .../GenerateResumeFromProfileUseCase.ts | 69 + .../use-cases/GenerateResumeUseCase.ts | 1 + src/domain/entities/NormalizedProfile.ts | 83 + src/domain/interfaces/IResumeRenderer.ts | 9 + src/domain/services/AtsAnalyzer.ts | 324 ++ src/domain/services/KeywordExtractor.ts | 44 + src/domain/services/ProfileMapper.ts | 117 + src/domain/services/ResumeOptimizer.ts | 202 + src/domain/services/TechTaxonomy.ts | 208 + src/domain/services/TextSanitizer.ts | 93 + src/http.ts | 11 + src/index.ts | 34 + .../enrichment/GitHubEnricher.ts | 270 ++ .../FileKeywordExtractor.ts | 31 +- .../renderers/DocxResumeRenderer.ts | 42 +- .../renderers/MarkdownResumeRenderer.ts | 102 +- .../renderers/PdfResumeRenderer.ts | 197 + .../renderers/RendererFactory.ts | 37 + src/presentation/http/apiKeyAuth.ts | 28 + src/presentation/http/createServer.ts | 46 + src/presentation/http/resumeController.ts | 59 + src/presentation/http/validation.ts | 94 + tests/atsAnalyzer.test.ts | 71 + tests/fixtures.ts | 157 + tests/githubEnricher.test.ts | 143 + tests/http.test.ts | 66 + tests/profileMapper.test.ts | 53 + tests/renderers.test.ts | 55 + tests/resumeOptimizer.test.ts | 47 + tests/techTaxonomy.test.ts | 60 + tests/textSanitizer.test.ts | 34 + vitest.config.ts | 9 + 38 files changed, 6639 insertions(+), 291 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 src/application/dtos/GenerateFromProfileInput.ts create mode 100644 src/application/use-cases/GenerateResumeFromProfileUseCase.ts create mode 100644 src/domain/entities/NormalizedProfile.ts create mode 100644 src/domain/services/AtsAnalyzer.ts create mode 100644 src/domain/services/KeywordExtractor.ts create mode 100644 src/domain/services/ProfileMapper.ts create mode 100644 src/domain/services/ResumeOptimizer.ts create mode 100644 src/domain/services/TechTaxonomy.ts create mode 100644 src/domain/services/TextSanitizer.ts create mode 100644 src/http.ts create mode 100644 src/index.ts create mode 100644 src/infrastructure/enrichment/GitHubEnricher.ts create mode 100644 src/infrastructure/renderers/PdfResumeRenderer.ts create mode 100644 src/infrastructure/renderers/RendererFactory.ts create mode 100644 src/presentation/http/apiKeyAuth.ts create mode 100644 src/presentation/http/createServer.ts create mode 100644 src/presentation/http/resumeController.ts create mode 100644 src/presentation/http/validation.ts create mode 100644 tests/atsAnalyzer.test.ts create mode 100644 tests/fixtures.ts create mode 100644 tests/githubEnricher.test.ts create mode 100644 tests/http.test.ts create mode 100644 tests/profileMapper.test.ts create mode 100644 tests/renderers.test.ts create mode 100644 tests/resumeOptimizer.test.ts create mode 100644 tests/techTaxonomy.test.ts create mode 100644 tests/textSanitizer.test.ts create mode 100644 vitest.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3317742 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +tests +*.log +resume.json +output +.git diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..081dec2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM node:22-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY tsconfig.json ./ +COPY src ./src +COPY config ./config +RUN npm run build + +FROM node:22-alpine AS runtime +WORKDIR /app +ENV NODE_ENV=production +COPY package*.json ./ +RUN npm ci --omit=dev +COPY --from=build /app/dist ./dist +COPY config ./config +ENV PORT=8089 +EXPOSE 8089 +CMD ["node", "dist/http.js"] diff --git a/README.md b/README.md index 45d5083..6c5fd50 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,72 @@ > **Template** — Gere currículos compatíveis com sistemas ATS nos formatos **DOCX** e **Markdown** a partir de um único arquivo JSON. > Desenvolvido com **TypeScript** e **Arquitetura Limpa (Clean Architecture)**. +## Dois modos de uso + +O ATS Forge funciona de duas formas: + +1. **CLI** (template original) — gera currículos a partir de `resume.json` + `config/`. +2. **Serviço HTTP (backend de currículos)** — recebe um **perfil normalizado** por HTTP + e devolve o currículo em **DOCX**, **PDF** ou **Markdown**, com um **ATS Score**. É + assim que a plataforma *Candidate* integra a geração de currículos. + +### Serviço HTTP + +```bash +npm install +npm run build + +# Sobe o serviço (porta padrão 8089; configurável via PORT/ATS_FORGE_PORT) +npm start +# ou, em desenvolvimento: +npm run serve +``` + +Variáveis de ambiente: + +| Variável | Descrição | +| ------------------- | ---------------------------------------------------------------------- | +| `PORT` | Porta do serviço (padrão `8089`). | +| `ATS_FORGE_API_KEY` | Se definido, exige o header `x-api-key`. Vazio = autenticação off. | + +#### `POST /resumes/generate` + +Corpo (JSON): + +```jsonc +{ + "profile": { + "name": "Ana Souza", + "headline": "Engenheira de Software Backend", + "summary": "...", // opcional; se ausente, é derivado das evidências + "contact": { "email": "...", "phone": "...", "website": "..." }, + "experience": [ { "company": "...", "role": "...", "period": "...", + "stack": ["..."], "highlights": ["..."], "results": ["..."] } ], + "education": [ { "institution": "...", "degree": "...", "field": "..." } ], + "skills": [ { "name": "TypeScript", "years": 5, "category": "Linguagens" } ], + "projects": [ { "name": "...", "description": "...", "stack": ["..."] } ], + "links": [ { "type": "github", "url": "..." } ], + "languages": [ { "name": "Português", "level": "Nativo" } ] + }, + "job": { "title": "...", "description": "..." }, // opcional + "format": "pdf" // "docx" | "pdf" | "md" +} +``` + +Resposta: o arquivo binário/texto no corpo, com: + +* `Content-Disposition: attachment; filename="..."` +* `X-Ats-Score: 0-100` — pontuação estimada de compatibilidade ATS. +* `X-Ats-Report: ` — relatório completo (breakdown, matched/missing keywords, sugestões). + +O motor **nunca inventa** empresas, cargos, datas, tecnologias ou resultados: ele apenas +organiza, resume e adapta palavras-chave a partir das evidências recebidas no perfil. + +--- + ## Recursos -* Gera currículos nos formatos `.docx` e `.md` para diferentes objetivos profissionais. +* Gera currículos nos formatos `.docx`, `.pdf` e `.md` para diferentes objetivos profissionais. * Extrai palavras-chave de uma descrição de vaga e as incorpora ao currículo. * Todos os seus dados pessoais ficam centralizados em um único arquivo `resume.json` (ignorado pelo Git — nunca é enviado ao repositório). * Toda a configuração dos perfis profissionais fica em arquivos JSON dentro da pasta `config/`, sem necessidade de alterar código TypeScript. diff --git a/package-lock.json b/package-lock.json index eeadc3c..305449f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,23 @@ "version": "2.0.0", "license": "MIT", "dependencies": { - "docx": "^9.6.1" + "cors": "^2.8.5", + "docx": "^9.6.1", + "express": "^4.21.2", + "pdfkit": "^0.15.1", + "zod": "^3.24.1" }, "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", "@types/node": "^20.19.39", + "@types/pdfkit": "^0.13.9", + "@types/supertest": "^6.0.2", "rimraf": "^5.0.10", + "supertest": "^7.0.0", "ts-node": "^10.9.2", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^2.1.8" } }, "node_modules/@cspotcode/source-map-support": { @@ -31,6 +41,397 @@ "node": ">=12" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -77,6 +478,49 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -88,31 +532,521 @@ "node": ">=14" } }, - "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==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.1.tgz", + "integrity": "sha512-QRU7UpoS9hd8Ij5I6fCVBxZ0UpdfAbZ5y7BWG5STFGMoM9jGzUP464yYAa3CzZxvG+jBwq1kI8L07xx0ReAdGA==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "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==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.1.tgz", + "integrity": "sha512-ttQF6Rp4MrNy9/2AOPmHmMXbNE8kIlNPYoGC3ksid9tonleMQALfrhTFJiQxobBvXYKG5Wcc8VGcHWTHR0ik8w==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "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==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.1.tgz", + "integrity": "sha512-MvzZ+qbBi5jrafURHsASUF1qW/58tOJFt/gFVJo2znQWrKX4CSf/Lt/qGONtoA4yHsNL5QzgdXMuITZxAs62Vg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "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==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.1.tgz", + "integrity": "sha512-9uj1VBuiPK5ij/+g0mCK17X+V/4SbErvRQMXIQjZAtbl4VcUhv63RhPZsFSWHrCRkTd3FKDtaVTrtT0+8bIhhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.1.tgz", + "integrity": "sha512-FCoXm836IqcXWhWPRkqlLx3Hjnv9S7c8tg5MFwsgHB3aFCz1JjUy6b1ssrxEjEbqaHpnvRmNjUUHxsXuU85NYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.1.tgz", + "integrity": "sha512-Xa6Oxo86Ilx1hq1vNb69541NOuUyhGEH3Sc0g7eGkfsfRblWh4hAa4Lj2GWwZyfUVYL+Z4xK3TxhwIuZ3fNPLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.1.tgz", + "integrity": "sha512-Nkw7cVMu1yrczpzr42Mh1BZ5Ai5A4Fsz2Je/frbwHmazpQTJzP+qo7pNjdxHrxVj56ivKTJihNoN90OD1hGxBA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.1.tgz", + "integrity": "sha512-JONaLM9mZPXbSetxMzYG3496qoJuWvG2ERCTrrVptfw/Fwe4UzrQoEx/mYEeZCRv/Xz0RldOLFeqDfqmH75/Zg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.1.tgz", + "integrity": "sha512-OfimHma+prmdoobHI3tfYud2enQ2LXYaMu+iMUQH7IjJkykJ6qUmAs1M/scdQaEKRWysiKwTctsDa2LXvaO0xw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.1.tgz", + "integrity": "sha512-X5fwh2HM8EHbgfFwpf4m2x2pG/AHdQ2aPDPk+JCDwV4DPeM8yxYmKG1vvm123JSc9gBSu8tu9NECA99wHPztsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.1.tgz", + "integrity": "sha512-8dp2TxKdjiX/4f7Da9gF7BR29hygxOQlHxKNa8qxJHoqZ13ytrogFTpMPhtVEFrgFu9MSOabAn006PeoygavjA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.1.tgz", + "integrity": "sha512-YuYDpRSZnU9qLf6sqyhiBdMRCw5ImECGLNqRjM2hZrJIEwAckameWkRJ3SUnn0oxSewZIib/4iLwyEXj40QqQQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.1.tgz", + "integrity": "sha512-gGj3v7YYkysy7IbfclMI4PDtHaa6YX4nU1D5NR1fyvXAGqM4TDk28ZWvrpOkkK92eoQRryAludWz6jNjsm77Qg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.1.tgz", + "integrity": "sha512-DTZVwvS94FUYpP0CjCfz2iT0XTmBfQGc/arx9IdwUbDImpe9h8dWuMT/oPUvcGiwLKMQoNmuFBx3AVajm+FNjA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.1.tgz", + "integrity": "sha512-NWnyiq2moc8iEHGyTjIFadJTy8VCvCU/rUBfor+3l30JOQKAifpmKdxX4ZxRRPSPE/rLhFpLADdzNjS3Ldctdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.1.tgz", + "integrity": "sha512-yIz+7L37g7bVOGLaUQmGqiYADH262krL4dtf5zU6QGldPgOairLEgW1FTs5pP0UFkHq5uf2qxDttOFsVAX6kxg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.1.tgz", + "integrity": "sha512-sRK+GWovJZV6ijyV/k1Ft3FL5+4DscF5OYsJolTbhVTTLFxKOPVjS0wxUUHakvL0vRw/cbFxZNeZLfM/EzuQhA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.1.tgz", + "integrity": "sha512-T2C0NXqqD063yJdm8UGdcoudBATDODdphEXWS7rEoh0jOITfoEBv7LHfRBYVsy+pI7wwxgScoZpkFiQOf40ryQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.1.tgz", + "integrity": "sha512-05wl8uRZMkcobB6YZrzskr2SiM0VOdzlFw3bAxOjKKTt3M6o3O0X/hE/8xk15u8oQY17xUCdTKcNMMg45ZAFog==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.1.tgz", + "integrity": "sha512-NoRZK1VclOgn9x3b9vPJ3sTxwvQ/F+Hf93xurRKSFJKiwNZyQZnLxVpqlCejhDvesMLMF8VNN+Y4jHTFddV/Xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.1.tgz", + "integrity": "sha512-SDkX4OkZBePUPf0/XsXVJLwS+O61CmkDKuIyk5CeWddK3vkE8AZjmWK+p1ILRez4wnbS5rTK+U9YmcbRyx2dHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.1.tgz", + "integrity": "sha512-JQ7el/Plr3juYFxIVu9YXDPNl57XR2L8WIc87JX9wj0L3goV8fXQPQXb3DaHVXq61X/WCGWsTCQsQHo6/Mz5UQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.1.tgz", + "integrity": "sha512-oZSlQ2H3ZDOz3ce1jEyuOZKuD0tPY/D2cKGMcMl0jhHQauBvkHHKAGR1OeRiMluvu3itzLRU2xCTUZptLayObA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.1.tgz", + "integrity": "sha512-Do2m3jUeVAvUDpee/yVZYRqhO8Ecx4x5sX3EVGalqLx+OxecLDGx3sIwPEm1Nq+MFK2FV3pgdpwWP4624b85Og==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.1.tgz", + "integrity": "sha512-KpxCwRn1Qh3tmBHAqIAF4HtUCVKqZd0w57TsItWblx3QVeJLtXaSgeBFAj1WAvBSpBo+Mh9iZCjj6YW/HaBVMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/helpers": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.3.17.tgz", + "integrity": "sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/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/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "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/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true, "license": "MIT" }, @@ -126,255 +1060,1718 @@ "undici-types": "~6.21.0" } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "node_modules/@types/pdfkit": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.13.9.tgz", + "integrity": "sha512-RDG8Yb1zT7I01FfpwK7nMSA433XWpblMqSCtA5vJlSyavWZb303HUYPCel6JTiDDFqwGLvtAnYbH8N/e0Cb89g==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@types/node": "*" + } + }, + "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": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/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": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" }, - "engines": { - "node": ">=0.4.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "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==", + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.11.0" + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.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/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "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/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/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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/body-parser": { + "version": "1.20.8", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz", + "integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==", + "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.16.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": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, + "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/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=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==", + "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/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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/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-disposition/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/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/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==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "deprecated": "Active development of CryptoJS has been discontinued. This library is no longer maintained.", + "license": "MIT" + }, + "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/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-equal/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "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/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/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, + "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/docx": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/docx/-/docx-9.6.1.tgz", + "integrity": "sha512-ZJja9/KBUuFC109sCMzovoq2GR2wCG/AuxivjA+OHj/q0TEgJIm3S7yrlUxIy3B+bV8YDj/BiHfWyrRFmyWpDQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^25.2.3", + "hash.js": "^1.1.7", + "jszip": "^3.10.1", + "nanoid": "^5.1.3", + "xml": "^1.0.1", + "xml-js": "^1.6.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/docx/node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/docx/node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "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/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/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/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-get-iterator/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "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/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "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/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.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/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz", + "integrity": "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "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.13", + "proxy-addr": "~2.0.7", + "qs": "~6.16.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/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/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/fontkit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.9.0.tgz", + "integrity": "sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.3.13", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "deep-equal": "^2.0.5", + "dfa": "^1.2.0", + "restructure": "^2.0.1", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.3.1", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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==", + "dev": true, + "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/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/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "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/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/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "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/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/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/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "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/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=0.4.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "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, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, "engines": { - "node": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "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/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/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" + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=7.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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==", - "license": "MIT" - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { - "node": ">= 8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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", + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "license": "MIT", "engines": { - "node": ">=0.3.1" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/docx": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/docx/-/docx-9.6.1.tgz", - "integrity": "sha512-ZJja9/KBUuFC109sCMzovoq2GR2wCG/AuxivjA+OHj/q0TEgJIm3S7yrlUxIy3B+bV8YDj/BiHfWyrRFmyWpDQ==", + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "license": "MIT", "dependencies": { - "@types/node": "^25.2.3", - "hash.js": "^1.1.7", - "jszip": "^3.10.1", - "nanoid": "^5.1.3", - "xml": "^1.0.1", - "xml-js": "^1.6.8" + "call-bound": "^1.0.3" }, "engines": { - "node": ">=10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/docx/node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "license": "MIT", "dependencies": { - "undici-types": "~7.19.0" - } - }, - "node_modules/docx/node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", - "license": "MIT" - }, - "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/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/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" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "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", + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "license": "MIT", "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" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, - "node_modules/inherits": { + "node_modules/is-weakset": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/is-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, + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/isarray": { @@ -406,6 +2803,13 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -427,6 +2831,32 @@ "immediate": "~3.0.5" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -434,6 +2864,16 @@ "dev": true, "license": "ISC" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -441,6 +2881,75 @@ "dev": true, "license": "ISC" }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/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/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -473,6 +2982,12 @@ "node": ">=16 || 14 >=14.17" } }, + "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/nanoid": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz", @@ -491,6 +3006,103 @@ "node": "^18 || >=20" } }, + "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/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/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/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", @@ -504,6 +3116,15 @@ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, + "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/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -531,12 +3152,177 @@ "url": "https://github.com/sponsors/isaacs" } }, + "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/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pdfkit": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.15.2.tgz", + "integrity": "sha512-s3GjpdBFSCaeDSX/v73MI5UsPqH1kjKut2AXCgxQ5OH10lPVOu5q5vLAG0OCpz/EYqKsTSw1WHpENqMvp43RKg==", + "license": "MIT", + "dependencies": { + "crypto-js": "^4.2.0", + "fontkit": "^1.8.1", + "jpeg-exif": "^1.1.4", + "linebreak": "^1.0.2", + "png-js": "^1.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/png-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.1.0.tgz", + "integrity": "sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==", + "dependencies": { + "browserify-zlib": "^0.2.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "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/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -552,6 +3338,32 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/restructure": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-2.0.1.tgz", + "integrity": "sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg==", + "license": "MIT" + }, "node_modules/rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", @@ -568,12 +3380,81 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "4.64.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.1.tgz", + "integrity": "sha512-7s4V+3J8ux75WNj9qwSJnrWmlCmZ2YHr2fojtHy5RiIMvhJv6DGe7HKKz7L6G1sq1Eg1fLC8SjYtAtDis1F4CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.64.1", + "@rollup/rollup-android-arm64": "4.64.1", + "@rollup/rollup-darwin-arm64": "4.64.1", + "@rollup/rollup-darwin-x64": "4.64.1", + "@rollup/rollup-freebsd-arm64": "4.64.1", + "@rollup/rollup-freebsd-x64": "4.64.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.1", + "@rollup/rollup-linux-arm-musleabihf": "4.64.1", + "@rollup/rollup-linux-arm64-gnu": "4.64.1", + "@rollup/rollup-linux-arm64-musl": "4.64.1", + "@rollup/rollup-linux-loong64-gnu": "4.64.1", + "@rollup/rollup-linux-loong64-musl": "4.64.1", + "@rollup/rollup-linux-ppc64-gnu": "4.64.1", + "@rollup/rollup-linux-ppc64-musl": "4.64.1", + "@rollup/rollup-linux-riscv64-gnu": "4.64.1", + "@rollup/rollup-linux-riscv64-musl": "4.64.1", + "@rollup/rollup-linux-s390x-gnu": "4.64.1", + "@rollup/rollup-linux-x64-gnu": "4.64.1", + "@rollup/rollup-linux-x64-musl": "4.64.1", + "@rollup/rollup-openbsd-x64": "4.64.1", + "@rollup/rollup-openharmony-arm64": "4.64.1", + "@rollup/rollup-win32-arm64-msvc": "4.64.1", + "@rollup/rollup-win32-ia32-msvc": "4.64.1", + "@rollup/rollup-win32-x64-gnu": "4.64.1", + "@rollup/rollup-win32-x64-msvc": "4.64.1", + "fsevents": "~2.3.2" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/sax": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", @@ -583,12 +3464,95 @@ "node": ">=11.0.0" } }, + "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==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, + "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", @@ -612,6 +3576,85 @@ "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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -619,10 +3662,56 @@ "dev": true, "license": "ISC", "engines": { - "node": ">=14" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "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/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">= 0.4" } }, "node_modules/string_decoder": { @@ -738,6 +3827,149 @@ "node": ">=8" } }, + "node_modules/superagent": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.4.1.tgz", + "integrity": "sha512-PVMkMrhKrSTtFhUU8jiWuGh/gyRMKiyCTCmm0+qVzRNxlfntmRKJkDhlEXN62x3HIb33sQ4kmL8lcSdccWAchQ==", + "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.3.1", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.3.1.tgz", + "integrity": "sha512-vNaNCFoCR/edDbErQZp5KLNT8R4dSPHX7rlDLoE8hWLAm/878j6ShMbe8HK/E6RwwuhlSXVelXAIg9AxR5WWPw==", + "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/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.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/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -782,6 +4014,25 @@ } } }, + "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-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/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -803,12 +4054,56 @@ "dev": true, "license": "MIT" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/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", @@ -816,6 +4111,214 @@ "dev": true, "license": "MIT" }, + "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/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/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/vite-node/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/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/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/vitest/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/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -832,6 +4335,81 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.24.tgz", + "integrity": "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -930,6 +4508,13 @@ "node": ">=8" } }, + "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/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", @@ -957,6 +4542,15 @@ "engines": { "node": ">=6" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 4689c78..de39ecf 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,23 @@ { "name": "ats-cv-generator", "version": "2.0.0", - "description": "ATS-friendly resume generator (DOCX + Markdown) — Clean Architecture, TypeScript template", - "main": "dist/main.js", + "description": "ATS resume-generation engine & microservice (DOCX + PDF + Markdown) — Clean Architecture, TypeScript", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], "scripts": { "build": "tsc", "generate": "ts-node src/main.ts", "generate:compiled": "node dist/main.js", + "serve": "ts-node src/http.ts", + "serve:compiled": "node dist/http.js", + "start": "node dist/http.js", "setup": "cp resume.example.json resume.json", - "clean": "rimraf dist" + "clean": "rimraf dist", + "test": "vitest run", + "test:watch": "vitest" }, "keywords": [ "resume", @@ -22,12 +31,22 @@ "license": "MIT", "type": "commonjs", "dependencies": { - "docx": "^9.6.1" + "cors": "^2.8.5", + "docx": "^9.6.1", + "express": "^4.21.2", + "pdfkit": "^0.15.1", + "zod": "^3.24.1" }, "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", "@types/node": "^20.19.39", + "@types/pdfkit": "^0.13.9", + "@types/supertest": "^6.0.2", "rimraf": "^5.0.10", + "supertest": "^7.0.0", "ts-node": "^10.9.2", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^2.1.8" } } diff --git a/src/application/dtos/GenerateFromProfileInput.ts b/src/application/dtos/GenerateFromProfileInput.ts new file mode 100644 index 0000000..eb7d3a7 --- /dev/null +++ b/src/application/dtos/GenerateFromProfileInput.ts @@ -0,0 +1,14 @@ +import { JobTarget, NormalizedProfile } from '../../domain/entities/NormalizedProfile'; +import { EnrichmentSources } from '../../infrastructure/enrichment/GitHubEnricher'; + +export type ResumeFormat = 'docx' | 'pdf' | 'md'; + +export interface GenerateFromProfileInput { + profile: NormalizedProfile; + job?: JobTarget | null; + format: ResumeFormat; + /** Public GitHub/LinkedIn links to enrich the profile from. */ + sources?: EnrichmentSources | null; + /** Optional base filename (without extension). Defaults to the candidate name. */ + filename?: string; +} diff --git a/src/application/use-cases/GenerateResumeFromProfileUseCase.ts b/src/application/use-cases/GenerateResumeFromProfileUseCase.ts new file mode 100644 index 0000000..b6f8557 --- /dev/null +++ b/src/application/use-cases/GenerateResumeFromProfileUseCase.ts @@ -0,0 +1,69 @@ +import { AtsReport } from '../../domain/services/AtsAnalyzer'; +import { mapProfileToRenderPayload } from '../../domain/services/ProfileMapper'; +import { ResumeOptimizer } from '../../domain/services/ResumeOptimizer'; +import { GitHubEnricher } from '../../infrastructure/enrichment/GitHubEnricher'; +import { createRenderer } from '../../infrastructure/renderers/RendererFactory'; +import { GenerateFromProfileInput } from '../dtos/GenerateFromProfileInput'; + +export interface GenerateResumeResult { + /** Rendered document — Buffer for docx/pdf, string for markdown. */ + content: Buffer | string; + filename: string; + contentType: string; + extension: string; + atsReport: AtsReport; + /** Non-fatal warnings (e.g. a GitHub import that failed). */ + warnings: string[]; +} + +/** + * Orchestrates resume generation from a normalized profile, with no file I/O: + * analyze the job → score → map profile to payload → render in the chosen + * format. Returns the document in memory so an HTTP layer can stream it. + */ +export class GenerateResumeFromProfileUseCase { + constructor( + private readonly optimizer: ResumeOptimizer = new ResumeOptimizer(), + private readonly enricher: GitHubEnricher = new GitHubEnricher(), + ) {} + + async execute(input: GenerateFromProfileInput): Promise { + // 1) Enrich with public GitHub data (profile + repo READMEs) + LinkedIn link. + const { profile, warnings } = await this.enricher.enrich( + input.profile, + input.sources ?? undefined, + ); + + // 2) Iteratively optimize toward the ATS pass threshold (evidence-based only). + const { profile: optimized, report: atsReport } = this.optimizer.optimize( + profile, + input.job, + ); + + // 3) Map to the render payload and render in the requested format. + const payload = mapProfileToRenderPayload(optimized, input.job, atsReport.matchedKeywords); + + const { renderer, contentType, extension } = createRenderer(input.format); + const content = await renderer.render(payload); + + const baseName = sanitizeFilename(input.filename ?? optimized.name ?? 'curriculo'); + + return { + content, + filename: `${baseName}${extension}`, + contentType, + extension, + atsReport, + warnings, + }; + } +} + +function sanitizeFilename(name: string): string { + const cleaned = name + .normalize('NFD') + .replace(/[̀-ͯ]/g, '') + .replace(/[^a-zA-Z0-9]+/g, '_') + .replace(/^_+|_+$/g, ''); + return cleaned || 'curriculo'; +} diff --git a/src/application/use-cases/GenerateResumeUseCase.ts b/src/application/use-cases/GenerateResumeUseCase.ts index 41637c6..a4fa517 100644 --- a/src/application/use-cases/GenerateResumeUseCase.ts +++ b/src/application/use-cases/GenerateResumeUseCase.ts @@ -77,6 +77,7 @@ export class GenerateResumeUseCase { profile, skills, experiencias, + projetos: [], extraKeywords, }; diff --git a/src/domain/entities/NormalizedProfile.ts b/src/domain/entities/NormalizedProfile.ts new file mode 100644 index 0000000..d30efe2 --- /dev/null +++ b/src/domain/entities/NormalizedProfile.ts @@ -0,0 +1,83 @@ + +export type DataSource = 'github' | 'linkedin' | 'candidate' | 'manual'; + +export interface NormalizedContact { + email?: string; + phone?: string; + location?: string; + website?: string; +} + +export interface NormalizedLink { + type: string; + url: string; + label?: string; +} + +export interface NormalizedExperience { + company: string; + role: string; + period?: string; + startDate?: string; + endDate?: string; + current?: boolean; + stack?: string[]; + highlights?: string[]; + results?: string[]; + source?: DataSource; +} + +export interface NormalizedEducation { + institution: string; + degree?: string; + field?: string; + period?: string; + source?: DataSource; +} + +export interface NormalizedSkill { + name: string; + years?: number; + category?: string; + source?: DataSource; +} + +export interface NormalizedProject { + name: string; + description?: string; + url?: string; + stack?: string[]; + highlights?: string[]; + source?: DataSource; +} + +export interface NormalizedLanguage { + name: string; + /** e.g. "Nativo", "Fluente", "Intermediário". */ + level?: string; +} + +export interface NormalizedProfile { + name: string; + /** Short professional title/headline, e.g. "Engenheiro de Software Backend". */ + headline?: string; + /** Professional summary. May be rewritten/reorganized by the engine, never invented. */ + summary?: string; + contact: NormalizedContact; + experience: NormalizedExperience[]; + education: NormalizedEducation[]; + skills: NormalizedSkill[]; + projects: NormalizedProject[]; + links: NormalizedLink[]; + languages: NormalizedLanguage[]; +} + +/** The vacancy the resume is being tailored to (optional). */ +export interface JobTarget { + title?: string; + description?: string; + url?: string; + /** Target language of the resume, e.g. "pt", "en". */ + language?: string; + seniority?: string; +} diff --git a/src/domain/interfaces/IResumeRenderer.ts b/src/domain/interfaces/IResumeRenderer.ts index 8f7d8fa..b8c2f82 100644 --- a/src/domain/interfaces/IResumeRenderer.ts +++ b/src/domain/interfaces/IResumeRenderer.ts @@ -11,6 +11,14 @@ export interface RenderContact { idiomas: string[]; } +export interface RenderProject { + name: string; + stack: string; + description: string; + highlights: string[]; + url?: string; +} + export interface RenderPayload { contact: RenderContact; focus: string; @@ -18,6 +26,7 @@ export interface RenderPayload { profile: string; skills: Record; experiencias: EnrichedExperience[]; + projetos: RenderProject[]; extraKeywords: string[]; } diff --git a/src/domain/services/AtsAnalyzer.ts b/src/domain/services/AtsAnalyzer.ts new file mode 100644 index 0000000..35db90a --- /dev/null +++ b/src/domain/services/AtsAnalyzer.ts @@ -0,0 +1,324 @@ +import { JobTarget, NormalizedProfile } from '../entities/NormalizedProfile'; +import { extractKeywordsFromText } from './KeywordExtractor'; +import { extractTechnologies, normalizeTechList } from './TechTaxonomy'; + +/** + * AtsAnalyzer v2 — scores a resume the way a real ATS + senior recruiter would, + * across six dimensions, and explains the result (breakdown, missing keywords, + * weak sections, recommendations). Evidence-based: a keyword/skill only counts + * when it is backed by the candidate's own data (spec §7/§9/§10/§13). + */ + +export interface JobAnalysis { + keywords: string[]; + /** Technologies required by the job (canonical). */ + hardSkills: string[]; + seniority: string | null; +} + +export interface AtsScoreBreakdown { + keywords: number; + experience: number; + technicalSkills: number; + structure: number; + achievements: number; + readability: number; +} + +export type AtsStatus = 'PASSED' | 'NEEDS_IMPROVEMENT' | 'INSUFFICIENT_DATA'; + +export interface AtsReport { + score: number; + status: AtsStatus; + breakdown: AtsScoreBreakdown; + matchedKeywords: string[]; + missingKeywords: string[]; + weakSections: string[]; + recommendations: string[]; +} + +const SENIORITY_TERMS: Array<[RegExp, string]> = [ + [/\b(est[aá]gi|intern|trainee)/i, 'Estágio/Trainee'], + [/\b(j[úu]nior|junior|jr)\b/i, 'Júnior'], + [/\b(pleno|mid[- ]?level|mid)\b/i, 'Pleno'], + [/\b(s[êe]nior|senior|sr)\b/i, 'Sênior'], + [/\b(especialista|specialist|staff|principal|lead|l[íi]der)\b/i, 'Especialista/Lead'], +]; + +const WEIGHTS: Record = { + keywords: 0.3, + experience: 0.2, + technicalSkills: 0.2, + structure: 0.15, + achievements: 0.08, + readability: 0.07, +}; + +const PASS_THRESHOLD = 80; + +const ACTION_VERBS = [ + 'desenvolv', 'implement', 'cri', 'constru', 'projet', 'lider', 'arquitet', + 'otimiz', 'integr', 'automat', 'migr', 'refator', 'mant', 'escal', 'reduz', + 'aument', 'entreg', 'coorden', 'gerenci', 'develop', 'implement', 'creat', + 'built', 'design', 'led', 'optimi', 'integrat', 'automat', 'migrat', 'deliver', +]; + +export class AtsAnalyzer { + analyzeJob(job: JobTarget | null | undefined): JobAnalysis { + const text = [job?.title ?? '', job?.description ?? ''].join('\n'); + const keywords = extractKeywordsFromText(text); + const hardSkills = extractTechnologies(text); + + let seniority: string | null = job?.seniority ?? null; + if (!seniority) { + for (const [re, label] of SENIORITY_TERMS) { + if (re.test(text)) { + seniority = label; + break; + } + } + } + return { keywords, hardSkills, seniority }; + } + + score(profile: NormalizedProfile, analysis: JobAnalysis): AtsReport { + const evidence = this.buildEvidenceSet(profile); + const profileTechs = this.profileTechnologies(profile); + + const { keywords, matched, missing } = this.scoreKeywords( + profile, + analysis, + evidence, + profileTechs, + ); + + const breakdown: AtsScoreBreakdown = { + keywords, + experience: this.scoreExperience(profile), + technicalSkills: this.scoreTechnicalSkills(profile, analysis, profileTechs), + structure: this.scoreStructure(profile), + achievements: this.scoreAchievements(profile), + readability: this.scoreReadability(profile), + }; + + const score = this.clamp( + Math.round( + (Object.keys(breakdown) as Array).reduce( + (sum, k) => sum + breakdown[k] * WEIGHTS[k], + 0, + ), + ), + ); + + const weakSections = this.findWeakSections(profile, breakdown); + const recommendations = this.buildRecommendations(profile, breakdown, missing); + + return { + score, + status: score >= PASS_THRESHOLD ? 'PASSED' : 'NEEDS_IMPROVEMENT', + breakdown, + matchedKeywords: matched, + missingKeywords: missing.slice(0, 20), + weakSections, + recommendations, + }; + } + + // ── Evidence ────────────────────────────────────────────────────────────────── + + private buildEvidenceSet(profile: NormalizedProfile): Set { + const text = this.profileText(profile); + const words = extractKeywordsFromText(text).map((w) => w.toLowerCase()); + const techs = extractTechnologies(text).map((t) => t.toLowerCase()); + return new Set([...words, ...techs]); + } + + private profileText(profile: NormalizedProfile): string { + return [ + profile.headline ?? '', + profile.summary ?? '', + ...profile.skills.map((s) => s.name), + ...profile.experience.flatMap((e) => [ + e.role, + e.company, + ...(e.stack ?? []), + ...(e.highlights ?? []), + ...(e.results ?? []), + ]), + ...profile.projects.flatMap((p) => [ + p.name, + p.description ?? '', + ...(p.stack ?? []), + ...(p.highlights ?? []), + ]), + ...profile.education.flatMap((ed) => [ed.degree ?? '', ed.field ?? '', ed.institution]), + ].join('\n'); + } + + private profileTechnologies(profile: NormalizedProfile): Set { + const fromSkills = profile.skills.map((s) => s.name); + const fromStacks = [ + ...profile.experience.flatMap((e) => e.stack ?? []), + ...profile.projects.flatMap((p) => p.stack ?? []), + ]; + const fromText = extractTechnologies(this.profileText(profile)); + return new Set( + normalizeTechList([...fromSkills, ...fromStacks, ...fromText]).map((t) => t.toLowerCase()), + ); + } + + // ── Dimension scores ──────────────────────────────────────────────────────── + + private scoreKeywords( + profile: NormalizedProfile, + analysis: JobAnalysis, + evidence: Set, + profileTechs: Set, + ): { keywords: number; matched: string[]; missing: string[] } { + if (analysis.keywords.length === 0) { + // No job: reward the breadth of the candidate's real keyword footprint. + const count = evidence.size; + const value = count >= 45 ? 92 : count >= 30 ? 85 : count >= 18 ? 72 : count >= 8 ? 55 : 32; + return { keywords: value, matched: [], missing: [] }; + } + + const isMatch = (k: string) => + evidence.has(k.toLowerCase()) || profileTechs.has(k.toLowerCase()); + const matched = analysis.keywords.filter(isMatch); + const missing = analysis.keywords.filter((k) => !isMatch(k)); + const coverage = Math.round((matched.length / analysis.keywords.length) * 100); + return { keywords: this.clamp(coverage), matched, missing }; + } + + private scoreTechnicalSkills( + profile: NormalizedProfile, + analysis: JobAnalysis, + profileTechs: Set, + ): number { + const categories = new Set( + profile.skills.map((s) => (s.category ?? '').trim()).filter(Boolean), + ); + const breadth = Math.min(100, profileTechs.size * 9 + categories.size * 6); + + if (analysis.hardSkills.length === 0) { + return profileTechs.size > 0 ? breadth : 30; + } + const covered = analysis.hardSkills.filter((s) => profileTechs.has(s.toLowerCase())).length; + const jobMatch = Math.round((covered / analysis.hardSkills.length) * 100); + // Blend job coverage with overall breadth. + return this.clamp(Math.round(jobMatch * 0.7 + breadth * 0.3)); + } + + private scoreExperience(profile: NormalizedProfile): number { + const entries = [ + ...profile.experience.map((e) => ({ bullets: (e.highlights ?? []).length, dated: Boolean(e.period || e.startDate) })), + ...profile.projects.map((p) => ({ bullets: (p.highlights ?? []).length + (p.description ? 1 : 0), dated: false })), + ]; + if (entries.length === 0) return 0; + + const countScore = Math.min(60, entries.length * 20); + const withBullets = entries.filter((e) => e.bullets > 0).length; + const bulletScore = Math.round((withBullets / entries.length) * 40); + return this.clamp(countScore + bulletScore); + } + + private scoreStructure(profile: NormalizedProfile): number { + const sections = [ + Boolean(profile.name?.trim()), + Boolean(profile.summary?.trim()), + Boolean(profile.contact.email?.trim()), + profile.skills.length > 0, + profile.experience.length > 0 || profile.projects.length > 0, + profile.education.length > 0, + profile.links.length > 0, + ]; + return Math.round((sections.filter(Boolean).length / sections.length) * 100); + } + + private scoreAchievements(profile: NormalizedProfile): number { + const bullets = [ + ...profile.experience.flatMap((e) => [...(e.highlights ?? []), ...(e.results ?? [])]), + ...profile.projects.flatMap((p) => p.highlights ?? []), + ]; + if (bullets.length === 0) return 0; + + const strong = bullets.filter((b) => { + const lower = b.toLowerCase(); + const hasVerb = ACTION_VERBS.some((v) => lower.includes(v)); + const hasNumber = /\d/.test(b); + return hasVerb || hasNumber; + }).length; + const ratio = strong / bullets.length; + // Reward both the ratio and having a reasonable number of bullets. + return this.clamp(Math.round(ratio * 80 + Math.min(20, bullets.length * 4))); + } + + private scoreReadability(profile: NormalizedProfile): number { + const bullets = [ + ...profile.experience.flatMap((e) => e.highlights ?? []), + ...profile.projects.flatMap((p) => p.highlights ?? []), + ]; + let score = 92; + const tooLong = bullets.filter((b) => b.length > 300).length; + if (bullets.length > 0) score -= Math.round((tooLong / bullets.length) * 30); + + // Penalize keyword stuffing in the summary. + if (profile.summary && this.isKeywordStuffed(profile.summary)) score -= 25; + if (!profile.summary?.trim()) score -= 10; + return this.clamp(score); + } + + private isKeywordStuffed(text: string): boolean { + const words = text.toLowerCase().split(/\s+/).filter((w) => w.length > 2); + if (words.length < 6) return false; + const counts = new Map(); + for (const w of words) counts.set(w, (counts.get(w) ?? 0) + 1); + const maxRepeat = Math.max(...counts.values()); + return maxRepeat >= 5; + } + + // ── Explanations ────────────────────────────────────────────────────────────── + + private findWeakSections(profile: NormalizedProfile, b: AtsScoreBreakdown): string[] { + const weak: string[] = []; + if (!profile.summary?.trim() || b.readability < 70) weak.push('Resumo Profissional'); + if (b.experience < 60) weak.push('Experiência / Projetos'); + if (b.technicalSkills < 60) weak.push('Habilidades Técnicas'); + if (b.achievements < 55) weak.push('Resultados / Conquistas'); + if (b.structure < 70) weak.push('Estrutura / Contato'); + return weak; + } + + private buildRecommendations( + profile: NormalizedProfile, + b: AtsScoreBreakdown, + missing: string[], + ): string[] { + const tips: string[] = []; + if (!profile.summary?.trim()) { + tips.push('Adicione um resumo profissional de 2–4 linhas no topo do currículo.'); + } + if (!profile.contact.email?.trim() || !profile.contact.phone?.trim()) { + tips.push('Inclua e-mail e telefone no cabeçalho.'); + } + if (b.experience < 60) { + tips.push('Conecte um GitHub/LinkedIn público ou descreva experiências com bullets de atividades.'); + } + if (b.achievements < 55) { + tips.push('Transforme responsabilidades em conquistas (verbo de ação + tecnologia + contexto + resultado).'); + } + if (b.keywords < 70 && missing.length > 0) { + tips.push( + `Evidencie (quando for verdade) termos da vaga ausentes: ${missing.slice(0, 6).join(', ')}.`, + ); + } + if (b.technicalSkills < 60) { + tips.push('Liste mais competências técnicas comprovadas, agrupadas por categoria.'); + } + return tips; + } + + private clamp(n: number): number { + return Math.max(0, Math.min(100, Math.round(n))); + } +} diff --git a/src/domain/services/KeywordExtractor.ts b/src/domain/services/KeywordExtractor.ts new file mode 100644 index 0000000..082194f --- /dev/null +++ b/src/domain/services/KeywordExtractor.ts @@ -0,0 +1,44 @@ +/** + * Pure keyword extraction (no I/O). Shared by the CLI file-based extractor and + * the HTTP job analyzer so both behave identically. + * + * The tokenizer deliberately preserves compound technical terms — "node.js", + * "c++", "c#", "ci/cd", "react-native", ".net" — instead of splitting them on + * punctuation, because those tokens are exactly what a recruiter and an ATS + * match a resume against. + */ + +const STOP_WORDS = new Set([ + 'e', 'de', 'da', 'do', 'em', 'com', 'para', 'por', 'os', 'as', + 'um', 'uma', 'o', 'a', 'que', 'se', 'na', 'no', 'ao', 'aos', + 'das', 'dos', 'nos', 'nas', 'sua', 'seu', 'ser', 'foi', 'são', + 'the', 'and', 'or', 'in', 'of', 'to', 'for', 'with', 'is', + 'are', 'you', 'we', 'our', 'be', 'at', 'on', 'an', 'us', 'it', + 'this', 'that', 'will', 'have', 'has', 'not', +]); + +const LETTERS = 'a-z0-9áéíóúàâêîôûãõçäëïöüñ'; +// A token is a run of letters/digits that may contain internal connectors +// (. + # / -), with connectors trimmed from the edges afterwards. +const TOKEN_RE = new RegExp(`[${LETTERS}][${LETTERS}+#./-]*[${LETTERS}+#]|[${LETTERS}]+`, 'gi'); + +export function extractKeywordsFromText(text: string): string[] { + const seen = new Set(); + const matches = text.toLowerCase().match(TOKEN_RE) ?? []; + + return matches + .map((w) => w.replace(/^[.+#/-]+/, '').replace(/[./-]+$/, '')) + .filter( + (w) => + w.length > 2 && + !STOP_WORDS.has(w) && + new RegExp(`[${LETTERS}]`, 'i').test(w), + ) + .filter((w) => { + if (seen.has(w)) return false; + seen.add(w); + return true; + }); +} + +export { STOP_WORDS }; diff --git a/src/domain/services/ProfileMapper.ts b/src/domain/services/ProfileMapper.ts new file mode 100644 index 0000000..14ce463 --- /dev/null +++ b/src/domain/services/ProfileMapper.ts @@ -0,0 +1,117 @@ +import { EnrichedExperience } from '../entities/EnrichedExperience'; +import { JobTarget, NormalizedProfile } from '../entities/NormalizedProfile'; +import { RenderPayload, RenderProject } from '../interfaces/IResumeRenderer'; +import { humanizeName, sanitizeText } from './TextSanitizer'; + +/** + * Maps the framework-agnostic NormalizedProfile onto the RenderPayload the + * renderers draw. It separates real professional experience from GitHub + * projects, sanitizes every string (removing emoji/HTML noise) and never + * invents data (spec §7). + */ + +const DEFAULT_SKILL_CATEGORY = 'Competências'; + +export function mapProfileToRenderPayload( + profile: NormalizedProfile, + job: JobTarget | null | undefined, + matchedKeywords: string[], +): RenderPayload { + return { + contact: { + nome: sanitizeText(profile.name), + email: sanitizeText(profile.contact.email) || '', + telefone: sanitizeText(profile.contact.phone) || '', + linkedin: findLink(profile, 'linkedin'), + github: findLink(profile, 'github'), + portfolio: profile.contact.website + ? sanitizeText(profile.contact.website) + : findLink(profile, 'portfolio'), + formacao: profile.education.map(formatEducation).filter(Boolean), + idiomas: profile.languages.map(formatLanguage).filter(Boolean), + }, + focus: 'candidate', + title: sanitizeText(job?.title) || sanitizeText(profile.headline) || 'Profissional', + profile: buildSummary(profile), + skills: buildSkills(profile), + experiencias: buildExperiences(profile), + projetos: buildProjects(profile), + extraKeywords: matchedKeywords.map((k) => sanitizeText(k)).filter(Boolean), + }; +} + +function findLink(profile: NormalizedProfile, type: string): string { + const link = profile.links.find((l) => l.type.toLowerCase() === type.toLowerCase()); + return link?.url ? sanitizeText(link.url) : ''; +} + +function formatEducation(ed: NormalizedProfile['education'][number]): string { + const degree = [ed.degree, ed.field].filter(Boolean).join(' em '); + const parts = [ed.institution, degree].filter((p) => p && p.trim()); + const base = sanitizeText(parts.join(' - ')); + return ed.period ? `${base} (${sanitizeText(ed.period)})` : base; +} + +function formatLanguage(lang: NormalizedProfile['languages'][number]): string { + return lang.level + ? `${sanitizeText(lang.name)} - ${sanitizeText(lang.level)}` + : sanitizeText(lang.name); +} + +function buildSummary(profile: NormalizedProfile): string { + if (profile.summary?.trim()) return sanitizeText(profile.summary); + + const topSkills = profile.skills.slice(0, 6).map((s) => sanitizeText(s.name)); + const role = sanitizeText(profile.headline); + if (role && topSkills.length > 0) { + return `${role} com experiência prática em ${topSkills.join(', ')}.`; + } + if (role) return role; + if (topSkills.length > 0) { + return `Profissional com experiência prática em ${topSkills.join(', ')}.`; + } + return ''; +} + +function buildSkills(profile: NormalizedProfile): Record { + const groups: Record = {}; + for (const skill of profile.skills) { + const category = sanitizeText(skill.category) || DEFAULT_SKILL_CATEGORY; + const name = sanitizeText(skill.name); + if (!name) continue; + if (!groups[category]) groups[category] = []; + if (!groups[category].includes(name)) groups[category].push(name); + } + return groups; +} + +function buildExperiences(profile: NormalizedProfile): EnrichedExperience[] { + return profile.experience.map((exp) => ({ + empresa: sanitizeText(exp.company), + cargo: sanitizeText(exp.role), + periodo: formatPeriod(exp), + stack: (exp.stack ?? []).map((s) => sanitizeText(s)).filter(Boolean).join(', '), + atividades: (exp.highlights ?? []).map((a) => sanitizeText(a)).filter(Boolean), + resultados: (exp.results ?? []).map((r) => sanitizeText(r)).filter(Boolean), + })); +} + +function buildProjects(profile: NormalizedProfile): RenderProject[] { + return profile.projects.map((proj) => ({ + name: humanizeName(sanitizeText(proj.name)), + stack: (proj.stack ?? []).map((s) => sanitizeText(s)).filter(Boolean).join(', '), + description: sanitizeText(proj.description), + // highlights excludes the description (which is rendered separately) to + // avoid the duplicated-bullet problem. + highlights: (proj.highlights ?? []) + .map((h) => sanitizeText(h)) + .filter((h) => h && h !== sanitizeText(proj.description)), + url: proj.url ? sanitizeText(proj.url) : undefined, + })); +} + +function formatPeriod(exp: NormalizedProfile['experience'][number]): string { + if (exp.period?.trim()) return sanitizeText(exp.period); + const end = exp.current ? 'Atual' : sanitizeText(exp.endDate); + return [sanitizeText(exp.startDate), end].filter(Boolean).join(' - '); +} diff --git a/src/domain/services/ResumeOptimizer.ts b/src/domain/services/ResumeOptimizer.ts new file mode 100644 index 0000000..e29310e --- /dev/null +++ b/src/domain/services/ResumeOptimizer.ts @@ -0,0 +1,202 @@ +import { + JobTarget, + NormalizedProfile, + NormalizedSkill, +} from '../entities/NormalizedProfile'; +import { AtsAnalyzer, AtsReport, AtsStatus, JobAnalysis } from './AtsAnalyzer'; +import { + categoryOf, + extractTechnologies, + normalizeTech, + normalizeTechList, +} from './TechTaxonomy'; + +/** + * Iteratively improves a resume toward the ATS pass threshold using ONLY + * legitimate, evidence-based transforms — normalization, deduplication, skill + * categorization, and an evidence-derived professional summary. It never invents + * data and never keyword-stuffs (spec §12/§13/§20). + */ + +const PASS_THRESHOLD = 80; +const MAX_ITERATIONS = 6; + +export interface OptimizationResult { + profile: NormalizedProfile; + report: AtsReport; + iterations: number; + appliedSteps: string[]; +} + +type Transform = { + name: string; + apply: (p: NormalizedProfile, ctx: Ctx) => NormalizedProfile; +}; + +interface Ctx { + job: JobTarget | null | undefined; + analysis: JobAnalysis; +} + +export class ResumeOptimizer { + constructor(private readonly analyzer: AtsAnalyzer = new AtsAnalyzer()) {} + + optimize(profile: NormalizedProfile, job: JobTarget | null | undefined): OptimizationResult { + const analysis = this.analyzer.analyzeJob(job); + const ctx: Ctx = { job, analysis }; + + // Cleanups are always safe and expected (normalize/dedupe/categorize/summary + // are core resume hygiene, not score-chasing). Each is applied only if it + // doesn't reduce the score — and we stop early once we comfortably pass, to + // avoid any artificial over-optimization (spec §13). + const transforms: Transform[] = [ + { name: 'normalize-technologies', apply: normalizeTechnologies }, + { name: 'categorize-skills', apply: categorizeSkills }, + { name: 'promote-evidenced-skills', apply: promoteEvidencedSkills }, + { name: 'generate-summary', apply: generateSummary }, + ]; + + let current = profile; + let report = this.analyzer.score(current, analysis); + const applied: string[] = []; + let iterations = 0; + + for (const transform of transforms) { + iterations++; + const candidate = transform.apply(current, ctx); + const candidateReport = this.analyzer.score(candidate, analysis); + + // Keep the transform only if it did not reduce the score. + if (candidateReport.score >= report.score) { + current = candidate; + report = candidateReport; + applied.push(transform.name); + } + + if (iterations >= MAX_ITERATIONS) break; + } + + report = { ...report, status: this.resolveStatus(current, report.score) }; + + return { profile: current, report, iterations, appliedSteps: applied }; + } + + private resolveStatus(profile: NormalizedProfile, score: number): AtsStatus { + if (score >= PASS_THRESHOLD) return 'PASSED'; + // Below threshold after legitimate optimization => not enough real data. + const hasRichData = + profile.skills.length >= 6 && + (profile.experience.length + profile.projects.length) >= 2; + return hasRichData ? 'NEEDS_IMPROVEMENT' : 'INSUFFICIENT_DATA'; + } +} + +// ── Transforms (pure) ─────────────────────────────────────────────────────────── + +function normalizeTechnologies(p: NormalizedProfile): NormalizedProfile { + const skills = dedupeSkills( + p.skills.map((s) => ({ ...s, name: normalizeTech(s.name) })), + ); + const experience = p.experience.map((e) => ({ + ...e, + stack: e.stack ? normalizeTechList(e.stack) : e.stack, + })); + const projects = p.projects.map((proj) => ({ + ...proj, + stack: proj.stack ? normalizeTechList(proj.stack) : proj.stack, + })); + return { ...p, skills, experience, projects }; +} + +function categorizeSkills(p: NormalizedProfile): NormalizedProfile { + const skills = p.skills.map((s) => { + const category = categoryOf(s.name) ?? s.category ?? 'Competências'; + return { ...s, category }; + }); + return { ...p, skills }; +} + +/** + * Adds, as skills, technologies that are PROVEN elsewhere in the profile + * (project/experience stacks, descriptions, READMEs) but not yet listed — real + * evidence, never invented. + */ +function promoteEvidencedSkills(p: NormalizedProfile): NormalizedProfile { + const existing = new Set(p.skills.map((s) => s.name.toLowerCase())); + + const evidenceText = [ + ...p.projects.flatMap((proj) => [ + proj.description ?? '', + ...(proj.stack ?? []), + ...(proj.highlights ?? []), + ]), + ...p.experience.flatMap((e) => [...(e.stack ?? []), ...(e.highlights ?? [])]), + p.summary ?? '', + p.headline ?? '', + ].join('\n'); + + const evidenced = extractTechnologies(evidenceText); + const additions: NormalizedSkill[] = []; + for (const tech of evidenced) { + if (!existing.has(tech.toLowerCase())) { + existing.add(tech.toLowerCase()); + additions.push({ + name: tech, + category: categoryOf(tech) ?? 'Competências', + source: 'github', + }); + } + } + return additions.length > 0 ? { ...p, skills: [...p.skills, ...additions] } : p; +} + +function generateSummary(p: NormalizedProfile, ctx: Ctx): NormalizedProfile { + if (p.summary && p.summary.trim().length >= 40) return p; + + const role = + ctx.job?.title?.trim() || + p.headline?.trim() || + 'Profissional de tecnologia'; + + const topTechs = normalizeTechList(p.skills.map((s) => s.name)).slice(0, 6); + + const categories = [...new Set(p.skills.map((s) => s.category).filter(Boolean))]; + const areas = categories + .filter((c) => c && c !== 'Competências') + .slice(0, 3) + .join(', '); + + const parts: string[] = []; + if (topTechs.length > 0) { + parts.push( + `${role} com experiência prática em ${topTechs.join(', ')}`, + ); + } else { + parts.push(role); + } + if (areas) parts.push(`Atuação em ${areas}`); + if (p.projects.length > 0) { + parts.push( + `Portfólio com ${p.projects.length} projeto(s) público(s) demonstrando as competências listadas`, + ); + } + + const summary = parts.join('. ') + '.'; + return { ...p, summary }; +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function dedupeSkills(skills: NormalizedSkill[]): NormalizedSkill[] { + const seen = new Map(); + for (const skill of skills) { + const key = skill.name.toLowerCase(); + const existing = seen.get(key); + if (!existing) { + seen.set(key, skill); + } else if (skill.years && (!existing.years || skill.years > existing.years)) { + seen.set(key, { ...existing, years: skill.years }); + } + } + return [...seen.values()]; +} diff --git a/src/domain/services/TechTaxonomy.ts b/src/domain/services/TechTaxonomy.ts new file mode 100644 index 0000000..20b0d68 --- /dev/null +++ b/src/domain/services/TechTaxonomy.ts @@ -0,0 +1,208 @@ +/** + * Technology taxonomy: canonical names, alias normalization, category mapping + * and extraction of known technologies from free text (READMEs, bios, job + * descriptions). This powers keyword coverage, deduplication and skill grouping. + * + * It only RECOGNIZES technologies that actually appear in the provided text — + * it never invents them (spec §3/§7/§18). + */ + +export type TechCategory = + | 'Linguagens' + | 'Backend' + | 'Frontend' + | 'Bancos de Dados' + | 'Infraestrutura & DevOps' + | 'Cloud' + | 'Testes' + | 'Arquitetura & Práticas' + | 'Ferramentas'; + +interface TechDef { + canonical: string; + category: TechCategory; + aliases: string[]; // lowercase; may contain spaces/dots/#/+ +} + +const TECHS: TechDef[] = [ + // Languages + { canonical: 'JavaScript', category: 'Linguagens', aliases: ['javascript', 'js'] }, + { canonical: 'TypeScript', category: 'Linguagens', aliases: ['typescript', 'ts'] }, + { canonical: 'Java', category: 'Linguagens', aliases: ['java'] }, + { canonical: 'Python', category: 'Linguagens', aliases: ['python', 'py'] }, + { canonical: 'Go', category: 'Linguagens', aliases: ['golang', 'go'] }, + { canonical: 'C#', category: 'Linguagens', aliases: ['c#', 'csharp', 'c-sharp'] }, + { canonical: 'C++', category: 'Linguagens', aliases: ['c++', 'cpp'] }, + { canonical: 'C', category: 'Linguagens', aliases: ['c'] }, + { canonical: 'PHP', category: 'Linguagens', aliases: ['php'] }, + { canonical: 'Ruby', category: 'Linguagens', aliases: ['ruby'] }, + { canonical: 'Rust', category: 'Linguagens', aliases: ['rust'] }, + { canonical: 'Kotlin', category: 'Linguagens', aliases: ['kotlin'] }, + { canonical: 'Swift', category: 'Linguagens', aliases: ['swift'] }, + { canonical: 'Dart', category: 'Linguagens', aliases: ['dart'] }, + { canonical: 'SQL', category: 'Linguagens', aliases: ['sql'] }, + { canonical: 'Shell', category: 'Linguagens', aliases: ['shell', 'bash', 'shell script'] }, + { canonical: 'HTML', category: 'Frontend', aliases: ['html', 'html5'] }, + { canonical: 'CSS', category: 'Frontend', aliases: ['css', 'css3'] }, + + // Backend / frameworks + { canonical: 'Node.js', category: 'Backend', aliases: ['node.js', 'nodejs', 'node'] }, + { canonical: 'Express.js', category: 'Backend', aliases: ['express.js', 'express', 'expressjs'] }, + { canonical: 'NestJS', category: 'Backend', aliases: ['nestjs', 'nest.js', 'nest'] }, + { canonical: 'Spring Boot', category: 'Backend', aliases: ['spring boot', 'springboot'] }, + { canonical: 'Spring', category: 'Backend', aliases: ['spring', 'spring framework'] }, + { canonical: 'Django', category: 'Backend', aliases: ['django'] }, + { canonical: 'Flask', category: 'Backend', aliases: ['flask'] }, + { canonical: 'FastAPI', category: 'Backend', aliases: ['fastapi'] }, + { canonical: '.NET', category: 'Backend', aliases: ['.net', 'dotnet', 'asp.net', 'aspnet'] }, + { canonical: 'Laravel', category: 'Backend', aliases: ['laravel'] }, + { canonical: 'Rails', category: 'Backend', aliases: ['rails', 'ruby on rails'] }, + { canonical: 'GraphQL', category: 'Backend', aliases: ['graphql'] }, + { canonical: 'MuleSoft', category: 'Backend', aliases: ['mulesoft', 'mule'] }, + + // Frontend + { canonical: 'React', category: 'Frontend', aliases: ['react', 'react.js', 'reactjs'] }, + { canonical: 'Next.js', category: 'Frontend', aliases: ['next.js', 'nextjs', 'next'] }, + { canonical: 'Vue.js', category: 'Frontend', aliases: ['vue.js', 'vuejs', 'vue'] }, + { canonical: 'Angular', category: 'Frontend', aliases: ['angular', 'angularjs'] }, + { canonical: 'Svelte', category: 'Frontend', aliases: ['svelte'] }, + { canonical: 'Tailwind CSS', category: 'Frontend', aliases: ['tailwind', 'tailwindcss', 'tailwind css'] }, + { canonical: 'Redux', category: 'Frontend', aliases: ['redux'] }, + { canonical: 'React Native', category: 'Frontend', aliases: ['react native', 'react-native'] }, + { canonical: 'Flutter', category: 'Frontend', aliases: ['flutter'] }, + + // Databases + { canonical: 'PostgreSQL', category: 'Bancos de Dados', aliases: ['postgresql', 'postgres', 'psql'] }, + { canonical: 'MySQL', category: 'Bancos de Dados', aliases: ['mysql'] }, + { canonical: 'MongoDB', category: 'Bancos de Dados', aliases: ['mongodb', 'mongo'] }, + { canonical: 'Redis', category: 'Bancos de Dados', aliases: ['redis', 'valkey'] }, + { canonical: 'SQLite', category: 'Bancos de Dados', aliases: ['sqlite'] }, + { canonical: 'SQL Server', category: 'Bancos de Dados', aliases: ['sql server', 'sqlserver', 'mssql'] }, + { canonical: 'Oracle', category: 'Bancos de Dados', aliases: ['oracle', 'oracle db'] }, + { canonical: 'Elasticsearch', category: 'Bancos de Dados', aliases: ['elasticsearch', 'elastic search'] }, + { canonical: 'DynamoDB', category: 'Bancos de Dados', aliases: ['dynamodb', 'dynamo'] }, + + // Infra / DevOps + { canonical: 'Docker', category: 'Infraestrutura & DevOps', aliases: ['docker'] }, + { canonical: 'Kubernetes', category: 'Infraestrutura & DevOps', aliases: ['kubernetes', 'k8s'] }, + { canonical: 'GitHub Actions', category: 'Infraestrutura & DevOps', aliases: ['github actions', 'gh actions'] }, + { canonical: 'GitLab CI', category: 'Infraestrutura & DevOps', aliases: ['gitlab ci', 'gitlab-ci'] }, + { canonical: 'Jenkins', category: 'Infraestrutura & DevOps', aliases: ['jenkins'] }, + { canonical: 'Terraform', category: 'Infraestrutura & DevOps', aliases: ['terraform'] }, + { canonical: 'CI/CD', category: 'Infraestrutura & DevOps', aliases: ['ci/cd', 'cicd', 'ci-cd'] }, + { canonical: 'Nginx', category: 'Infraestrutura & DevOps', aliases: ['nginx'] }, + { canonical: 'Linux', category: 'Infraestrutura & DevOps', aliases: ['linux'] }, + { canonical: 'Kafka', category: 'Infraestrutura & DevOps', aliases: ['kafka', 'apache kafka'] }, + { canonical: 'RabbitMQ', category: 'Infraestrutura & DevOps', aliases: ['rabbitmq'] }, + + // Cloud + { canonical: 'AWS', category: 'Cloud', aliases: ['aws', 'amazon web services'] }, + { canonical: 'Google Cloud', category: 'Cloud', aliases: ['gcp', 'google cloud', 'google cloud platform'] }, + { canonical: 'Azure', category: 'Cloud', aliases: ['azure', 'microsoft azure'] }, + { canonical: 'Vercel', category: 'Cloud', aliases: ['vercel'] }, + { canonical: 'Heroku', category: 'Cloud', aliases: ['heroku'] }, + + // Testing + { canonical: 'Jest', category: 'Testes', aliases: ['jest'] }, + { canonical: 'Vitest', category: 'Testes', aliases: ['vitest'] }, + { canonical: 'JUnit', category: 'Testes', aliases: ['junit'] }, + { canonical: 'Cypress', category: 'Testes', aliases: ['cypress'] }, + { canonical: 'Playwright', category: 'Testes', aliases: ['playwright'] }, + { canonical: 'Testing Library', category: 'Testes', aliases: ['testing library', 'testing-library'] }, + { canonical: 'Pytest', category: 'Testes', aliases: ['pytest'] }, + + // Architecture / practices + { canonical: 'REST API', category: 'Arquitetura & Práticas', aliases: ['rest api', 'rest apis', 'rest', 'restful'] }, + { canonical: 'Microservices', category: 'Arquitetura & Práticas', aliases: ['microservices', 'microservice', 'microsserviços', 'microsservicos'] }, + { canonical: 'Clean Architecture', category: 'Arquitetura & Práticas', aliases: ['clean architecture', 'arquitetura limpa'] }, + { canonical: 'DDD', category: 'Arquitetura & Práticas', aliases: ['ddd', 'domain driven design', 'domain-driven design'] }, + { canonical: 'TDD', category: 'Arquitetura & Práticas', aliases: ['tdd', 'test driven development'] }, + { canonical: 'SOLID', category: 'Arquitetura & Práticas', aliases: ['solid'] }, + { canonical: 'Agile', category: 'Arquitetura & Práticas', aliases: ['agile', 'scrum', 'kanban', 'ágil'] }, + { canonical: 'gRPC', category: 'Arquitetura & Práticas', aliases: ['grpc'] }, + { canonical: 'WebSockets', category: 'Arquitetura & Práticas', aliases: ['websocket', 'websockets'] }, + + // Tools + { canonical: 'Git', category: 'Ferramentas', aliases: ['git'] }, + { canonical: 'Prisma', category: 'Ferramentas', aliases: ['prisma'] }, + { canonical: 'Drizzle ORM', category: 'Ferramentas', aliases: ['drizzle', 'drizzle orm'] }, + { canonical: 'TypeORM', category: 'Ferramentas', aliases: ['typeorm'] }, + { canonical: 'Swagger', category: 'Ferramentas', aliases: ['swagger', 'openapi'] }, +]; + +// Build lookup maps. +const ALIAS_TO_CANONICAL = new Map(); +const CANONICAL_TO_CATEGORY = new Map(); +for (const def of TECHS) { + CANONICAL_TO_CATEGORY.set(def.canonical, def.category); + ALIAS_TO_CANONICAL.set(def.canonical.toLowerCase(), def.canonical); + for (const alias of def.aliases) ALIAS_TO_CANONICAL.set(alias, def.canonical); +} + +// Aliases sorted by length desc so multi-word terms match before their parts +// ("spring boot" before "spring", "react native" before "react"). +const SORTED_ALIASES = [...ALIAS_TO_CANONICAL.keys()].sort((a, b) => b.length - a.length); + +function escapeRegExp(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** Normalizes a single technology token to its canonical name (or trims it). */ +export function normalizeTech(raw: string): string { + const key = raw.trim().toLowerCase(); + return ALIAS_TO_CANONICAL.get(key) ?? raw.trim(); +} + +/** Canonical category for a (canonical or alias) tech name. */ +export function categoryOf(tech: string): TechCategory | undefined { + const canonical = normalizeTech(tech); + return CANONICAL_TO_CATEGORY.get(canonical); +} + +/** + * Extracts the set of known technologies mentioned in free text, returned as + * canonical names (deduplicated). Longer aliases are matched first and the + * matched span is blanked out, so compound terms ("Spring Boot") win over their + * parts ("Spring"). Boundaries use look-behind/ahead so adjacent tokens still + * match. + */ +export function extractTechnologies(text: string): string[] { + if (!text) return []; + let haystack = text.toLowerCase(); + const seen = new Set(); + const found: string[] = []; + + for (const alias of SORTED_ALIASES) { + const canonical = ALIAS_TO_CANONICAL.get(alias)!; + const pattern = new RegExp( + `(? { + matched = true; + return ' '.repeat(m.length); // blank the span so sub-aliases can't re-match + }); + if (matched && !seen.has(canonical)) { + seen.add(canonical); + found.push(canonical); + } + } + + return found; +} + +/** Normalizes + dedupes a list of tech names to canonical form. */ +export function normalizeTechList(list: string[]): string[] { + const seen = new Set(); + const out: string[] = []; + for (const item of list) { + const canonical = normalizeTech(item); + const key = canonical.toLowerCase(); + if (!seen.has(key)) { + seen.add(key); + out.push(canonical); + } + } + return out; +} diff --git a/src/domain/services/TextSanitizer.ts b/src/domain/services/TextSanitizer.ts new file mode 100644 index 0000000..ae7ab49 --- /dev/null +++ b/src/domain/services/TextSanitizer.ts @@ -0,0 +1,93 @@ +/** + * Sanitizes free text for resume rendering: decodes common HTML entities, + * strips emoji / pictographs / markdown badge noise and any character outside + * the printable Latin-1 range the PDF/DOCX base fonts can render — while + * preserving Portuguese accents (á, ã, ç, é, õ, …). This is what fixes the + * garbled "Ø=Üh" emoji artifacts coming from GitHub READMEs/bios. + */ + +const HTML_ENTITIES: Record = { + ' ': ' ', + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", + ''': "'", + '—': '-', + '–': '-', + '…': '...', +}; + +function decodeEntities(text: string): string { + let out = text; + for (const [entity, value] of Object.entries(HTML_ENTITIES)) { + out = out.split(entity).join(value); + } + // numeric entities + out = out.replace(/&#(\d+);/g, (_, code) => { + const n = Number(code); + return Number.isFinite(n) ? String.fromCodePoint(n) : ''; + }); + return out; +} + +/** Normalize characters that commonly appear but aren't in the base fonts. */ +function normalizeTypography(text: string): string { + return text + .replace(/[‘’‚‛]/g, "'") // smart single quotes + .replace(/[“”„‟]/g, '"') // smart double quotes + .replace(/[–—―]/g, '-') // en/em dashes -> hyphen + .replace(/…/g, '...') // ellipsis + .replace(/[•●▪·]/g, '-') // stray bullets in source text + .replace(/ /g, ' '); // nbsp +} + +/** + * Removes emoji, pictographs, symbols, variation selectors and any remaining + * character outside the Latin-1 printable set. Keeps standard whitespace and + * accented Latin letters. + */ +function stripNonRenderable(text: string): string { + let out = ''; + for (const ch of text) { + const code = ch.codePointAt(0)!; + const isTabOrNewline = ch === '\n' || ch === '\t'; + const isBasicPrintable = code >= 0x20 && code <= 0x7e; + const isLatin1Printable = code >= 0xa0 && code <= 0xff; + if (isTabOrNewline || isBasicPrintable || isLatin1Printable) { + out += ch; + } else { + // Drop emoji / symbols / CJK / etc. Insert a space to avoid word merges. + out += ' '; + } + } + return out; +} + +export function sanitizeText(input: string | null | undefined): string { + if (!input) return ''; + const decoded = decodeEntities(input); + const normalized = normalizeTypography(decoded); + const cleaned = stripNonRenderable(normalized); + return cleaned.replace(/[ \t]{2,}/g, ' ').replace(/\s*\n\s*/g, '\n').trim(); +} + +/** Sanitizes and, if the text is empty afterwards, returns undefined. */ +export function sanitizeOptional(input: string | null | undefined): string | undefined { + const cleaned = sanitizeText(input); + return cleaned.length > 0 ? cleaned : undefined; +} + +/** Humanizes a repo/slug name: "node-monitoring" / "CARDS_ICON" -> "Node Monitoring". */ +export function humanizeName(name: string): string { + const spaced = name + .replace(/[_-]+/g, ' ') + .replace(/([a-z])([A-Z])/g, '$1 $2') + .replace(/\s+/g, ' ') + .trim(); + return spaced + .split(' ') + .map((w) => (w.length <= 3 ? w : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())) + .join(' '); +} diff --git a/src/http.ts b/src/http.ts new file mode 100644 index 0000000..eaca2b0 --- /dev/null +++ b/src/http.ts @@ -0,0 +1,11 @@ +import { createServer } from './presentation/http/createServer'; + +const port = Number(process.env.PORT ?? process.env.ATS_FORGE_PORT ?? 8089); +const apiKey = process.env.ATS_FORGE_API_KEY?.trim() || undefined; + +const app = createServer({ apiKey }); + +app.listen(port, () => { + // eslint-disable-next-line no-console + console.log(`[ats-forge] serviço de currículos ouvindo em http://localhost:${port}`); +}); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..738b642 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,34 @@ +/** Public programmatic API of ATS Forge (resume-generation engine). */ + +export * from './domain/entities/NormalizedProfile'; +export { AtsAnalyzer } from './domain/services/AtsAnalyzer'; +export type { + AtsReport, + AtsScoreBreakdown, + AtsStatus, + JobAnalysis, +} from './domain/services/AtsAnalyzer'; +export { ResumeOptimizer } from './domain/services/ResumeOptimizer'; +export type { OptimizationResult } from './domain/services/ResumeOptimizer'; +export { + categoryOf, + extractTechnologies, + normalizeTech, + normalizeTechList, +} from './domain/services/TechTaxonomy'; +export type { TechCategory } from './domain/services/TechTaxonomy'; +export { extractKeywordsFromText } from './domain/services/KeywordExtractor'; +export { mapProfileToRenderPayload } from './domain/services/ProfileMapper'; + +export { GenerateResumeFromProfileUseCase } from './application/use-cases/GenerateResumeFromProfileUseCase'; +export type { GenerateResumeResult } from './application/use-cases/GenerateResumeFromProfileUseCase'; +export type { + GenerateFromProfileInput, + ResumeFormat, +} from './application/dtos/GenerateFromProfileInput'; + +export { GitHubEnricher, parseGitHubUsername } from './infrastructure/enrichment/GitHubEnricher'; +export type { EnrichmentSources, EnrichmentResult } from './infrastructure/enrichment/GitHubEnricher'; +export { createRenderer } from './infrastructure/renderers/RendererFactory'; +export { createServer } from './presentation/http/createServer'; +export type { ServerOptions } from './presentation/http/createServer'; diff --git a/src/infrastructure/enrichment/GitHubEnricher.ts b/src/infrastructure/enrichment/GitHubEnricher.ts new file mode 100644 index 0000000..b4029cd --- /dev/null +++ b/src/infrastructure/enrichment/GitHubEnricher.ts @@ -0,0 +1,270 @@ +import { + NormalizedProfile, + NormalizedProject, + NormalizedSkill, +} from '../../domain/entities/NormalizedProfile'; +import { + categoryOf, + extractTechnologies, + normalizeTechList, +} from '../../domain/services/TechTaxonomy'; + +/** + * Enriches a normalized profile with PUBLIC GitHub data: the profile, the + * profile README, the public repositories and the READMEs of the most relevant + * ones. Technologies are extracted from languages, topics, descriptions and + * README text, then normalized to canonical names. + * + * Only public, authorized data is used — no auth tokens, no scraping, no private + * data. Everything merged is real (tagged `source: "github"`); nothing is + * fabricated (spec §2/§3/§7/§24). + */ + +export interface EnrichmentSources { + github?: string; + linkedin?: string; +} + +export interface EnrichmentResult { + profile: NormalizedProfile; + warnings: string[]; +} + +interface GitHubUser { + login: string; + name?: string | null; + bio?: string | null; + blog?: string | null; + location?: string | null; + company?: string | null; + html_url: string; +} + +interface GitHubRepo { + name: string; + description?: string | null; + html_url: string; + language?: string | null; + stargazers_count: number; + forks_count: number; + fork: boolean; + archived: boolean; + topics?: string[]; + pushed_at?: string; +} + +interface GitHubReadme { + content?: string; + encoding?: string; +} + +const API = 'https://api.github.com'; +const TIMEOUT_MS = 8000; +const MAX_PROJECTS = 5; +const MAX_README_FETCHES = 5; +const MAX_SKILLS_FROM_GITHUB = 20; + +export class GitHubEnricher { + async enrich( + profile: NormalizedProfile, + sources: EnrichmentSources | undefined, + ): Promise { + const warnings: string[] = []; + let enriched = profile; + + const linkedin = normalizeLinkedIn(sources?.linkedin); + if (linkedin) enriched = addLink(enriched, 'linkedin', linkedin); + + const username = parseGitHubUsername(sources?.github); + if (!username) return { profile: enriched, warnings }; + + try { + const [user, repos] = await Promise.all([ + this.fetchJson(`${API}/users/${username}`), + this.fetchJson( + `${API}/users/${username}/repos?per_page=100&sort=pushed`, + ), + ]); + + const relevant = (repos ?? []) + .filter((r) => !r.fork && !r.archived) + .sort(byRelevance) + .slice(0, Math.max(MAX_PROJECTS, MAX_README_FETCHES)); + + // Read the profile README + the top repo READMEs (best-effort, in parallel). + const [profileReadme, ...repoReadmes] = await Promise.all([ + this.fetchReadme(username, username), + ...relevant + .slice(0, MAX_README_FETCHES) + .map((r) => this.fetchReadme(username, r.name)), + ]); + + enriched = this.mergeGitHub(enriched, user, relevant, profileReadme, repoReadmes); + } catch (err) { + warnings.push( + `Não foi possível importar o GitHub "${username}": ${(err as Error).message}. O currículo foi gerado com os demais dados.`, + ); + } + + return { profile: enriched, warnings }; + } + + private mergeGitHub( + profile: NormalizedProfile, + user: GitHubUser, + repos: GitHubRepo[], + profileReadme: string | null, + repoReadmes: Array, + ): NormalizedProfile { + // ── Technologies from every public signal ──────────────────────────────── + const techText = [ + user.bio ?? '', + profileReadme ?? '', + ...repos.map((r) => r.description ?? ''), + ...repos.flatMap((r) => r.topics ?? []), + ...repoReadmes.filter((x): x is string => Boolean(x)), + ].join('\n'); + + const languages = repos.map((r) => r.language).filter((l): l is string => Boolean(l)); + const technologies = normalizeTechList([ + ...languages, + ...extractTechnologies(techText), + ]).slice(0, MAX_SKILLS_FROM_GITHUB); + + const existingSkillNames = new Set(profile.skills.map((s) => s.name.toLowerCase())); + const githubSkills: NormalizedSkill[] = technologies + .filter((t) => !existingSkillNames.has(t.toLowerCase())) + .map((t) => ({ + name: t, + category: categoryOf(t) ?? 'Tecnologias (GitHub)', + source: 'github' as const, + })); + + // ── Projects from the top repos, enriched with README-derived tech ─────── + const topProjects: NormalizedProject[] = repos + .filter((r) => r.description || r.stargazers_count > 0 || r.topics?.length) + .slice(0, MAX_PROJECTS) + .map((r, idx) => { + const readme = repoReadmes[idx] ?? ''; + const stack = normalizeTechList([ + ...(r.language ? [r.language] : []), + ...(r.topics ?? []), + ...extractTechnologies(`${r.description ?? ''}\n${readme}`), + ]).slice(0, 8); + return { + name: r.name, + description: r.description ?? undefined, + url: r.html_url, + stack: stack.length ? stack : undefined, + highlights: buildRepoHighlights(r), + source: 'github' as const, + }; + }); + + // ── Summary / contact fallbacks (never override candidate-provided data) ── + // We intentionally do NOT use the raw profile README as the summary: READMEs + // are full of emoji, badges and markdown that produce garbled output. We use + // the short, clean bio as a fallback and let the optimizer craft a clean, + // evidence-based summary otherwise. (profileReadme is still used for tech + // extraction above.) + void profileReadme; + const summary = + profile.summary || (user.bio && user.bio.length <= 240 ? user.bio : undefined); + + let merged: NormalizedProfile = { + ...profile, + summary: summary?.trim() || undefined, + contact: { + ...profile.contact, + location: profile.contact.location || user.location || undefined, + website: profile.contact.website || normalizeUrl(user.blog) || undefined, + }, + skills: [...profile.skills, ...githubSkills], + projects: [...profile.projects, ...topProjects], + }; + + merged = addLink(merged, 'github', user.html_url); + const site = normalizeUrl(user.blog); + if (site) merged = addLink(merged, 'portfolio', site); + + return merged; + } + + private async fetchJson(url: string): Promise { + const res = await fetch(url, { + headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'ats-forge' }, + signal: AbortSignal.timeout(TIMEOUT_MS), + }); + if (res.status === 404) throw new Error('perfil não encontrado'); + if (res.status === 403) throw new Error('limite de requisições do GitHub atingido'); + if (!res.ok) throw new Error(`GitHub respondeu HTTP ${res.status}`); + return (await res.json()) as T; + } + + /** Fetches and decodes a repo's README. Returns null if there is none. */ + private async fetchReadme(owner: string, repo: string): Promise { + try { + const res = await fetch(`${API}/repos/${owner}/${repo}/readme`, { + headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'ats-forge' }, + signal: AbortSignal.timeout(TIMEOUT_MS), + }); + if (!res.ok) return null; + const body = (await res.json()) as GitHubReadme; + if (!body.content) return null; + const buf = Buffer.from(body.content, (body.encoding as BufferEncoding) ?? 'base64'); + return buf.toString('utf-8'); + } catch { + return null; + } + } +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function byRelevance(a: GitHubRepo, b: GitHubRepo): number { + // Stars first, then most recently pushed. + if (b.stargazers_count !== a.stargazers_count) { + return b.stargazers_count - a.stargazers_count; + } + return (b.pushed_at ?? '').localeCompare(a.pushed_at ?? ''); +} + +function buildRepoHighlights(r: GitHubRepo): string[] { + // Description is rendered separately by the mapper, so it is NOT repeated here. + const highlights: string[] = []; + const metrics: string[] = []; + if (r.stargazers_count > 0) metrics.push(`${r.stargazers_count} stars`); + if (r.forks_count > 0) metrics.push(`${r.forks_count} forks`); + if (metrics.length > 0) highlights.push(`Projeto open source (${metrics.join(', ')}).`); + return highlights; +} + +export function parseGitHubUsername(input: string | undefined): string | null { + if (!input) return null; + const trimmed = input.trim(); + if (!trimmed) return null; + const urlMatch = /github\.com\/([A-Za-z0-9-]+)/i.exec(trimmed); + const candidate = urlMatch ? urlMatch[1] : trimmed.replace(/^@/, ''); + return /^[A-Za-z0-9-]{1,39}$/.test(candidate) ? candidate : null; +} + +function normalizeLinkedIn(input: string | undefined): string | null { + if (!input) return null; + const trimmed = input.trim(); + if (!trimmed) return null; + if (/^https?:\/\//i.test(trimmed)) return trimmed; + if (/linkedin\.com/i.test(trimmed)) return `https://${trimmed}`; + return `https://www.linkedin.com/in/${trimmed.replace(/^@/, '')}`; +} + +function normalizeUrl(input: string | null | undefined): string | undefined { + if (!input) return undefined; + const trimmed = input.trim(); + if (!trimmed) return undefined; + return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`; +} + +function addLink(profile: NormalizedProfile, type: string, url: string): NormalizedProfile { + if (profile.links.some((l) => l.type.toLowerCase() === type.toLowerCase())) return profile; + return { ...profile, links: [...profile.links, { type, url }] }; +} diff --git a/src/infrastructure/keyword-extractors/FileKeywordExtractor.ts b/src/infrastructure/keyword-extractors/FileKeywordExtractor.ts index 5db33a9..aeba911 100644 --- a/src/infrastructure/keyword-extractors/FileKeywordExtractor.ts +++ b/src/infrastructure/keyword-extractors/FileKeywordExtractor.ts @@ -1,14 +1,6 @@ import fs from 'fs'; import { IKeywordExtractor } from '../../domain/interfaces/IKeywordExtractor'; - -const STOP_WORDS = new Set([ - 'e', 'de', 'da', 'do', 'em', 'com', 'para', 'por', 'os', 'as', - 'um', 'uma', 'o', 'a', 'que', 'se', 'na', 'no', 'ao', 'aos', - 'das', 'dos', 'nos', 'nas', 'sua', 'seu', 'ser', 'foi', 'são', - 'the', 'and', 'or', 'in', 'of', 'to', 'for', 'with', 'is', - 'are', 'you', 'we', 'our', 'be', 'at', 'on', 'an', 'us', 'it', - 'this', 'that', 'will', 'have', 'has', 'not', -]); +import { extractKeywordsFromText } from '../../domain/services/KeywordExtractor'; export class FileKeywordExtractor implements IKeywordExtractor { extract(filePath: string | null): string[] { @@ -19,25 +11,6 @@ export class FileKeywordExtractor implements IKeywordExtractor { return []; } - return this.extractFromText(fs.readFileSync(filePath, 'utf-8')); - } - - private extractFromText(text: string): string[] { - const seen = new Set(); - return text - .toLowerCase() - .split(/[\s,.\-/()\[\]{}:;|+&'"!\n\r\t]+/) - .map((w) => w.trim()) - .filter( - (w) => - w.length > 2 && - !STOP_WORDS.has(w) && - /[a-záéíóúàãõêâçA-Z0-9]/.test(w), - ) - .filter((w) => { - if (seen.has(w)) return false; - seen.add(w); - return true; - }); + return extractKeywordsFromText(fs.readFileSync(filePath, 'utf-8')); } } diff --git a/src/infrastructure/renderers/DocxResumeRenderer.ts b/src/infrastructure/renderers/DocxResumeRenderer.ts index 6a7efdd..59fd1f5 100644 --- a/src/infrastructure/renderers/DocxResumeRenderer.ts +++ b/src/infrastructure/renderers/DocxResumeRenderer.ts @@ -8,7 +8,11 @@ import { TextRun, } from 'docx'; import { EnrichedExperience } from '../../domain/entities/EnrichedExperience'; -import { IResumeRenderer, RenderPayload } from '../../domain/interfaces/IResumeRenderer'; +import { + IResumeRenderer, + RenderPayload, + RenderProject, +} from '../../domain/interfaces/IResumeRenderer'; // ── Typography constants (values in half-points) ───────────────────────────── const FONT = 'Arial'; @@ -114,6 +118,31 @@ function experienceBlock(experiencias: EnrichedExperience[]): Paragraph[] { return children; } +function projectsBlock(projetos: RenderProject[]): Paragraph[] { + if (projetos.length === 0) return []; + const children: Paragraph[] = [sectionHeader('Projetos')]; + + projetos.forEach((proj, idx) => { + if (idx > 0) children.push(gap(120)); + + children.push(new Paragraph({ + children: [tx(proj.name, { bold: true, size: SZ_CORPO })], + spacing: { before: 60, after: 20, ...SPACELINE }, + })); + + if (proj.stack) { + children.push(new Paragraph({ + children: [tx(`Stack: ${proj.stack}`, { italics: true, size: SZ_DETALHE, color: '555555' })], + spacing: { after: 40, ...SPACELINE }, + })); + } + if (proj.description) children.push(bulletItem(proj.description)); + proj.highlights.forEach((h) => children.push(bulletItem(h))); + }); + + return children; +} + function educationBlock(formacao: string[]): Paragraph[] { return [sectionHeader('Formação Acadêmica'), ...formacao.map(bulletItem)]; } @@ -141,15 +170,16 @@ export class DocxResumeRenderer implements IResumeRenderer { readonly extension = '.docx'; async render(payload: RenderPayload): Promise { - const { contact, title, profile, skills, experiencias } = payload; + const { contact, title, profile, skills, experiencias, projetos } = payload; const children = [ ...contactBlock(contact, title), - ...summaryBlock(profile), - ...experienceBlock(experiencias), - ...educationBlock(contact.formacao), + ...(profile ? summaryBlock(profile) : []), + ...(experiencias.length > 0 ? experienceBlock(experiencias) : []), + ...projectsBlock(projetos), ...skillsBlock(skills), - ...languagesBlock(contact.idiomas), + ...(contact.formacao.length > 0 ? educationBlock(contact.formacao) : []), + ...(contact.idiomas.length > 0 ? languagesBlock(contact.idiomas) : []), ]; const doc = new Document({ diff --git a/src/infrastructure/renderers/MarkdownResumeRenderer.ts b/src/infrastructure/renderers/MarkdownResumeRenderer.ts index 40c61d3..fb8b8f8 100644 --- a/src/infrastructure/renderers/MarkdownResumeRenderer.ts +++ b/src/infrastructure/renderers/MarkdownResumeRenderer.ts @@ -4,72 +4,80 @@ export class MarkdownResumeRenderer implements IResumeRenderer { readonly extension = '.md'; async render(payload: RenderPayload): Promise { - const { contact, title, profile, skills, experiencias, extraKeywords } = payload; + const { contact, title, profile, skills, experiencias, projetos, extraKeywords } = payload; const keywordsSection = extraKeywords.length > 0 ? `\n> **Palavras-chave da vaga:** ${extraKeywords.slice(0, 12).join(', ')}\n` : ''; - const experiencesSection = experiencias - .map((exp) => { - const activities = exp.atividades.map((a) => `- ${a}`).join('\n'); - const results = - exp.resultados.length > 0 - ? `\n**Resultados Relevantes:**\n${exp.resultados.map((r) => `- ${r}`).join('\n')}` - : ''; - return [ - `**${exp.empresa} – ${exp.cargo}**`, - `${exp.periodo} | Stack: ${exp.stack}`, - '', - activities, - results, - ] - .join('\n') - .trimEnd(); - }) - .join('\n\n---\n\n'); + const contactLine = [contact.email, contact.telefone].filter(Boolean).join(' | '); + const linksLine = [contact.linkedin, contact.github, contact.portfolio] + .filter(Boolean) + .join(' | '); + + const experienceSection = + experiencias.length > 0 + ? `\n---\n\n## EXPERIÊNCIA PROFISSIONAL\n\n${experiencias + .map((exp) => { + const activities = exp.atividades.map((a) => `- ${a}`).join('\n'); + const results = + exp.resultados.length > 0 + ? `\n**Resultados:**\n${exp.resultados.map((r) => `- ${r}`).join('\n')}` + : ''; + const meta = [exp.periodo, exp.stack ? `Stack: ${exp.stack}` : ''] + .filter(Boolean) + .join(' | '); + return [`**${exp.empresa} – ${exp.cargo}**`, meta, '', activities, results] + .join('\n') + .trimEnd(); + }) + .join('\n\n')}\n` + : ''; + + const projectsSection = + projetos.length > 0 + ? `\n---\n\n## PROJETOS\n\n${projetos + .map((p) => { + const meta = p.stack ? `Stack: ${p.stack}` : ''; + const bullets = [p.description, ...p.highlights] + .filter(Boolean) + .map((b) => `- ${b}`) + .join('\n'); + return [`**${p.name}**`, meta, '', bullets].filter(Boolean).join('\n').trimEnd(); + }) + .join('\n\n')}\n` + : ''; const skillsSection = Object.entries(skills) .map(([cat, items]) => `**${cat}:** ${items.join(', ')}`) .join('\n'); - return `# ${contact.nome} -## ${title} - -${contact.email} | ${contact.telefone} -${contact.linkedin} | ${contact.github} -Portfólio: ${contact.portfolio} -${keywordsSection} ---- - -## RESUMO PROFISSIONAL - -${profile} - ---- - -## EXPERIÊNCIA PROFISSIONAL - -${experiencesSection} + const educationSection = + contact.formacao.length > 0 + ? `\n---\n\n## FORMAÇÃO ACADÊMICA\n\n${contact.formacao.map((f) => `- ${f}`).join('\n')}\n` + : ''; ---- + const languagesSection = + contact.idiomas.length > 0 + ? `\n---\n\n## IDIOMAS\n\n${contact.idiomas.map((i) => `- ${i}`).join('\n')}\n` + : ''; -## FORMAÇÃO ACADÊMICA + const summarySection = profile + ? `\n---\n\n## RESUMO PROFISSIONAL\n\n${profile}\n` + : ''; -${contact.formacao.map((f) => `- ${f}`).join('\n')} + return `# ${contact.nome} +## ${title} +${contactLine} +${linksLine} +${keywordsSection}${summarySection}${experienceSection}${projectsSection} --- ## HABILIDADES TÉCNICAS ${skillsSection} - ---- - -## IDIOMAS - -${contact.idiomas.map((i) => `- ${i}`).join('\n')} -`; +${educationSection}${languagesSection}`; } } diff --git a/src/infrastructure/renderers/PdfResumeRenderer.ts b/src/infrastructure/renderers/PdfResumeRenderer.ts new file mode 100644 index 0000000..97eaf04 --- /dev/null +++ b/src/infrastructure/renderers/PdfResumeRenderer.ts @@ -0,0 +1,197 @@ +import PDFDocument from 'pdfkit'; +import { EnrichedExperience } from '../../domain/entities/EnrichedExperience'; +import { + IResumeRenderer, + RenderPayload, + RenderProject, +} from '../../domain/interfaces/IResumeRenderer'; + +/** + * PDF renderer built for ATS parsing: a single column, real selectable text + * (never an image), semantic section headers and simple bullet lists. No + * tables, columns, icons or skill bars — exactly what ATS parsers read cleanly + * (integration spec §12/§13). + */ + +const FONT = 'Helvetica'; +const FONT_BOLD = 'Helvetica-Bold'; +const FONT_ITALIC = 'Helvetica-Oblique'; +const COLOR_TEXT = '#1a1a1a'; +const COLOR_MUTED = '#555555'; +const COLOR_RULE = '#444444'; +const COLOR_RESULT = '#1a6611'; +const MARGIN = 56; // ~0.78in + +export class PdfResumeRenderer implements IResumeRenderer { + readonly extension = '.pdf'; + + render(payload: RenderPayload): Promise { + const { contact, title, profile, skills, experiencias } = payload; + + return new Promise((resolve, reject) => { + const doc = new PDFDocument({ + size: 'A4', + margins: { top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN }, + info: { + Title: `${title} – ${contact.nome}`, + Author: contact.nome, + Subject: title, + Keywords: Object.values(skills).flat().join(', '), + }, + }); + + const chunks: Buffer[] = []; + doc.on('data', (c: Buffer) => chunks.push(c)); + doc.on('end', () => resolve(Buffer.concat(chunks))); + doc.on('error', reject); + + try { + this.drawHeader(doc, contact, title); + this.drawSummary(doc, profile); + if (experiencias.length > 0) this.drawExperience(doc, experiencias); + this.drawProjects(doc, payload.projetos); + this.drawSkills(doc, skills); + this.drawEducation(doc, contact.formacao); + this.drawLanguages(doc, contact.idiomas); + doc.end(); + } catch (err) { + reject(err as Error); + } + }); + } + + private sectionHeader(doc: PDFKit.PDFDocument, label: string): void { + doc.moveDown(0.7); + doc + .font(FONT_BOLD) + .fontSize(11) + .fillColor(COLOR_TEXT) + .text(label.toUpperCase()); + const y = doc.y + 2; + doc + .moveTo(doc.page.margins.left, y) + .lineTo(doc.page.width - doc.page.margins.right, y) + .lineWidth(0.75) + .strokeColor(COLOR_RULE) + .stroke(); + doc.moveDown(0.5); + } + + private drawHeader( + doc: PDFKit.PDFDocument, + contact: RenderPayload['contact'], + title: string, + ): void { + doc.font(FONT_BOLD).fontSize(20).fillColor(COLOR_TEXT).text(contact.nome.toUpperCase(), { + align: 'center', + }); + doc.font(FONT).fontSize(12).fillColor(COLOR_MUTED).text(title, { align: 'center' }); + doc.moveDown(0.3); + + const contactLine = [contact.email, contact.telefone].filter(Boolean).join(' | '); + const linksLine = [contact.linkedin, contact.github, contact.portfolio] + .filter(Boolean) + .join(' | '); + + doc.font(FONT).fontSize(10).fillColor(COLOR_TEXT); + if (contactLine) doc.text(contactLine, { align: 'center' }); + if (linksLine) doc.text(linksLine, { align: 'center' }); + } + + private drawSummary(doc: PDFKit.PDFDocument, profile: string): void { + if (!profile.trim()) return; + this.sectionHeader(doc, 'Resumo Profissional'); + doc.font(FONT).fontSize(10.5).fillColor(COLOR_TEXT).text(profile, { align: 'left' }); + } + + private drawExperience(doc: PDFKit.PDFDocument, experiencias: EnrichedExperience[]): void { + if (experiencias.length === 0) return; + this.sectionHeader(doc, 'Experiência Profissional'); + + experiencias.forEach((exp, idx) => { + if (idx > 0) doc.moveDown(0.5); + + doc + .font(FONT_BOLD) + .fontSize(10.5) + .fillColor(COLOR_TEXT) + .text(`${exp.empresa}${exp.cargo ? ` – ${exp.cargo}` : ''}`); + + const meta = [exp.periodo, exp.stack ? `Stack: ${exp.stack}` : ''] + .filter(Boolean) + .join(' | '); + if (meta) { + doc.font(FONT_ITALIC).fontSize(9).fillColor(COLOR_MUTED).text(meta); + } + doc.moveDown(0.2); + + exp.atividades.forEach((a) => this.bullet(doc, a)); + + if (exp.resultados.length > 0) { + doc.moveDown(0.2); + doc + .font(FONT_BOLD) + .fontSize(10) + .fillColor(COLOR_TEXT) + .text('Resultados:', { indent: 12 }); + exp.resultados.forEach((r) => this.bullet(doc, r, COLOR_RESULT)); + } + }); + } + + private drawProjects(doc: PDFKit.PDFDocument, projetos: RenderProject[]): void { + if (projetos.length === 0) return; + this.sectionHeader(doc, 'Projetos'); + + projetos.forEach((proj, idx) => { + if (idx > 0) doc.moveDown(0.4); + + doc.font(FONT_BOLD).fontSize(10.5).fillColor(COLOR_TEXT).text(proj.name); + if (proj.stack) { + doc.font(FONT_ITALIC).fontSize(9).fillColor(COLOR_MUTED).text(`Stack: ${proj.stack}`); + } + doc.moveDown(0.1); + if (proj.description) this.bullet(doc, proj.description); + proj.highlights.forEach((h) => this.bullet(doc, h)); + }); + } + + private drawEducation(doc: PDFKit.PDFDocument, formacao: string[]): void { + if (formacao.length === 0) return; + this.sectionHeader(doc, 'Formação Acadêmica'); + formacao.forEach((f) => this.bullet(doc, f)); + } + + private drawSkills(doc: PDFKit.PDFDocument, skills: Record): void { + const entries = Object.entries(skills); + if (entries.length === 0) return; + this.sectionHeader(doc, 'Habilidades Técnicas'); + for (const [category, items] of entries) { + doc.fontSize(10).fillColor(COLOR_TEXT); + doc + .font(FONT_BOLD) + .text(`${category}: `, { continued: true }) + .font(FONT) + .text(items.join(' | ')); + } + } + + private drawLanguages(doc: PDFKit.PDFDocument, idiomas: string[]): void { + if (idiomas.length === 0) return; + this.sectionHeader(doc, 'Idiomas'); + idiomas.forEach((i) => this.bullet(doc, i)); + } + + private bullet( + doc: PDFKit.PDFDocument, + text: string, + color: string = COLOR_TEXT, + marker = '•', + ): void { + doc + .font(FONT) + .fontSize(10) + .fillColor(color) + .text(`${marker} ${text}`, { indent: 12, align: 'left' }); + } +} diff --git a/src/infrastructure/renderers/RendererFactory.ts b/src/infrastructure/renderers/RendererFactory.ts new file mode 100644 index 0000000..167be31 --- /dev/null +++ b/src/infrastructure/renderers/RendererFactory.ts @@ -0,0 +1,37 @@ +import { IResumeRenderer } from '../../domain/interfaces/IResumeRenderer'; +import { ResumeFormat } from '../../application/dtos/GenerateFromProfileInput'; +import { DocxResumeRenderer } from './DocxResumeRenderer'; +import { MarkdownResumeRenderer } from './MarkdownResumeRenderer'; +import { PdfResumeRenderer } from './PdfResumeRenderer'; + +export interface RendererInfo { + renderer: IResumeRenderer; + contentType: string; + extension: string; +} + +const CONTENT_TYPES: Record = { + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + pdf: 'application/pdf', + md: 'text/markdown; charset=utf-8', +}; + +export function createRenderer(format: ResumeFormat): RendererInfo { + switch (format) { + case 'pdf': + return { renderer: new PdfResumeRenderer(), contentType: CONTENT_TYPES.pdf, extension: '.pdf' }; + case 'md': + return { + renderer: new MarkdownResumeRenderer(), + contentType: CONTENT_TYPES.md, + extension: '.md', + }; + case 'docx': + default: + return { + renderer: new DocxResumeRenderer(), + contentType: CONTENT_TYPES.docx, + extension: '.docx', + }; + } +} diff --git a/src/presentation/http/apiKeyAuth.ts b/src/presentation/http/apiKeyAuth.ts new file mode 100644 index 0000000..12a29c4 --- /dev/null +++ b/src/presentation/http/apiKeyAuth.ts @@ -0,0 +1,28 @@ +import { NextFunction, Request, Response } from 'express'; + +/** + * Service-to-service auth. The candidate backend (the only caller) must send the + * shared secret in the `x-api-key` header. If no key is configured the guard is + * disabled (useful for local development / tests), and a warning is logged once. + */ +export function apiKeyAuth(expectedKey: string | undefined) { + let warned = false; + return (req: Request, res: Response, next: NextFunction): void => { + if (!expectedKey) { + if (!warned) { + // eslint-disable-next-line no-console + console.warn('[ats-forge] ATS_FORGE_API_KEY não definido — autenticação desabilitada.'); + warned = true; + } + next(); + return; + } + + const provided = req.header('x-api-key'); + if (!provided || provided !== expectedKey) { + res.status(401).json({ code: 'UNAUTHORIZED', message: 'API key inválida ou ausente.' }); + return; + } + next(); + }; +} diff --git a/src/presentation/http/createServer.ts b/src/presentation/http/createServer.ts new file mode 100644 index 0000000..6c4fdd8 --- /dev/null +++ b/src/presentation/http/createServer.ts @@ -0,0 +1,46 @@ +import cors from 'cors'; +import express, { NextFunction, Request, Response } from 'express'; +import { apiKeyAuth } from './apiKeyAuth'; +import { generateResumeHandler } from './resumeController'; + +export interface ServerOptions { + apiKey?: string; + /** Allowed origin(s) for CORS. Defaults to reflecting the request origin. */ + corsOrigin?: string | string[] | boolean; + /** Max JSON body size. Job descriptions can be long. */ + jsonLimit?: string; +} + +/** + * Builds the ATS Forge HTTP app (resume-generation microservice). Kept as a + * factory with no side effects so it can be imported directly by tests. + */ +export function createServer(options: ServerOptions = {}) { + const app = express(); + + app.disable('x-powered-by'); + app.use(cors({ origin: options.corsOrigin ?? true })); + app.use(express.json({ limit: options.jsonLimit ?? '1mb' })); + + app.get('/health', (_req: Request, res: Response) => { + res.json({ ok: true, service: 'ats-forge', ts: new Date().toISOString() }); + }); + + app.post('/resumes/generate', apiKeyAuth(options.apiKey), (req, res, next) => { + generateResumeHandler(req, res).catch(next); + }); + + // Fallback 404 + app.use((_req: Request, res: Response) => { + res.status(404).json({ code: 'NOT_FOUND', message: 'Rota não encontrada.' }); + }); + + // Centralized error handler + app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => { + // eslint-disable-next-line no-console + console.error('[ats-forge] erro não tratado:', err); + res.status(500).json({ code: 'INTERNAL_ERROR', message: 'Erro ao gerar currículo.' }); + }); + + return app; +} diff --git a/src/presentation/http/resumeController.ts b/src/presentation/http/resumeController.ts new file mode 100644 index 0000000..f772411 --- /dev/null +++ b/src/presentation/http/resumeController.ts @@ -0,0 +1,59 @@ +import { Request, Response } from 'express'; +import { ZodError } from 'zod'; +import { GenerateResumeFromProfileUseCase } from '../../application/use-cases/GenerateResumeFromProfileUseCase'; +import { generateResumeSchema } from './validation'; + +const useCase = new GenerateResumeFromProfileUseCase(); + +/** + * POST /resumes/generate + * + * Receives a normalized profile + optional job, returns the rendered resume as a + * binary/text body. The ATS analysis travels in response headers so the caller + * can surface the score without parsing the document: + * - X-Ats-Score: final 0-100 score + * - X-Ats-Report: base64-encoded JSON of the full AtsReport + */ +export async function generateResumeHandler(req: Request, res: Response): Promise { + let body; + try { + body = generateResumeSchema.parse(req.body); + } catch (err) { + if (err instanceof ZodError) { + res.status(400).json({ + code: 'VALIDATION_ERROR', + message: 'Perfil normalizado inválido.', + details: err.flatten().fieldErrors, + }); + return; + } + throw err; + } + + const result = await useCase.execute({ + profile: body.profile, + job: body.job ?? null, + sources: body.sources ?? null, + format: body.format, + filename: body.filename, + }); + + const reportJson = JSON.stringify({ ...result.atsReport, warnings: result.warnings }); + res.setHeader('Content-Type', result.contentType); + res.setHeader( + 'Content-Disposition', + `attachment; filename="${result.filename}"`, + ); + res.setHeader('X-Ats-Score', String(result.atsReport.score)); + res.setHeader('X-Ats-Report', Buffer.from(reportJson, 'utf-8').toString('base64')); + res.setHeader( + 'Access-Control-Expose-Headers', + 'X-Ats-Score, X-Ats-Report, Content-Disposition', + ); + + if (Buffer.isBuffer(result.content)) { + res.status(200).send(result.content); + } else { + res.status(200).send(Buffer.from(result.content, 'utf-8')); + } +} diff --git a/src/presentation/http/validation.ts b/src/presentation/http/validation.ts new file mode 100644 index 0000000..7504e22 --- /dev/null +++ b/src/presentation/http/validation.ts @@ -0,0 +1,94 @@ +import { z } from 'zod'; + +const dataSource = z.enum(['github', 'linkedin', 'candidate', 'manual']); + +const contactSchema = z.object({ + email: z.string().email().optional().or(z.literal('')), + phone: z.string().max(40).optional(), + location: z.string().max(120).optional(), + website: z.string().max(300).optional(), +}); + +const linkSchema = z.object({ + type: z.string().min(1).max(40), + url: z.string().max(500), + label: z.string().max(120).optional(), +}); + +const experienceSchema = z.object({ + company: z.string().min(1).max(200), + role: z.string().min(1).max(200), + period: z.string().max(100).optional(), + startDate: z.string().max(40).optional(), + endDate: z.string().max(40).optional(), + current: z.boolean().optional(), + stack: z.array(z.string().max(80)).max(60).optional(), + highlights: z.array(z.string().max(1000)).max(40).optional(), + results: z.array(z.string().max(1000)).max(40).optional(), + source: dataSource.optional(), +}); + +const educationSchema = z.object({ + institution: z.string().min(1).max(200), + degree: z.string().max(200).optional(), + field: z.string().max(200).optional(), + period: z.string().max(100).optional(), + source: dataSource.optional(), +}); + +const skillSchema = z.object({ + name: z.string().min(1).max(80), + years: z.number().min(0).max(70).optional(), + category: z.string().max(80).optional(), + source: dataSource.optional(), +}); + +const projectSchema = z.object({ + name: z.string().min(1).max(200), + description: z.string().max(2000).optional(), + url: z.string().max(500).optional(), + stack: z.array(z.string().max(80)).max(60).optional(), + highlights: z.array(z.string().max(1000)).max(40).optional(), + source: dataSource.optional(), +}); + +const languageSchema = z.object({ + name: z.string().min(1).max(80), + level: z.string().max(80).optional(), +}); + +export const normalizedProfileSchema = z.object({ + name: z.string().min(1).max(200), + headline: z.string().max(200).optional(), + summary: z.string().max(4000).optional(), + contact: contactSchema.default({}), + experience: z.array(experienceSchema).max(60).default([]), + education: z.array(educationSchema).max(40).default([]), + skills: z.array(skillSchema).max(200).default([]), + projects: z.array(projectSchema).max(60).default([]), + links: z.array(linkSchema).max(40).default([]), + languages: z.array(languageSchema).max(20).default([]), +}); + +export const jobTargetSchema = z.object({ + title: z.string().max(200).optional(), + description: z.string().max(20000).optional(), + url: z.string().max(500).optional(), + language: z.string().max(10).optional(), + seniority: z.string().max(60).optional(), +}); + +export const sourcesSchema = z.object({ + github: z.string().max(300).optional(), + linkedin: z.string().max(300).optional(), +}); + +export const generateResumeSchema = z.object({ + profile: normalizedProfileSchema, + job: jobTargetSchema.nullish(), + sources: sourcesSchema.nullish(), + format: z.enum(['docx', 'pdf', 'md']).default('docx'), + filename: z.string().max(120).optional(), +}); + +export type GenerateResumeBody = z.infer; diff --git a/tests/atsAnalyzer.test.ts b/tests/atsAnalyzer.test.ts new file mode 100644 index 0000000..7ab7e4c --- /dev/null +++ b/tests/atsAnalyzer.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from 'vitest'; +import { AtsAnalyzer } from '../src/domain/services/AtsAnalyzer'; +import { fullProfile, minimalProfile } from './fixtures'; + +describe('AtsAnalyzer v2', () => { + const analyzer = new AtsAnalyzer(); + + it('extrai keywords, hard skills e senioridade da vaga', () => { + const analysis = analyzer.analyzeJob({ + title: 'Desenvolvedor Backend Sênior', + description: 'Experiência com Node.js, TypeScript e PostgreSQL.', + }); + + expect(analysis.keywords).toContain('node.js'); + expect(analysis.hardSkills).toEqual( + expect.arrayContaining(['Node.js', 'TypeScript', 'PostgreSQL']), + ); + expect(analysis.seniority).toBe('Sênior'); + }); + + it('só conta keywords com evidência no perfil (sem inventar)', () => { + const analysis = analyzer.analyzeJob({ + description: 'Buscamos TypeScript, Node.js, Kubernetes e Docker.', + }); + const report = analyzer.score(fullProfile(), analysis); + + expect(report.matchedKeywords).toEqual( + expect.arrayContaining(['typescript', 'node.js']), + ); + expect(report.missingKeywords).toEqual(expect.arrayContaining(['kubernetes'])); + expect(report.matchedKeywords).not.toContain('kubernetes'); + }); + + it('retorna score, status e breakdown de 6 dimensões', () => { + const report = analyzer.score( + fullProfile(), + analyzer.analyzeJob({ description: 'Node.js e TypeScript' }), + ); + + expect(report.score).toBeGreaterThanOrEqual(0); + expect(report.score).toBeLessThanOrEqual(100); + expect(report.breakdown).toHaveProperty('keywords'); + expect(report.breakdown).toHaveProperty('experience'); + expect(report.breakdown).toHaveProperty('technicalSkills'); + expect(report.breakdown).toHaveProperty('structure'); + expect(report.breakdown).toHaveProperty('achievements'); + expect(report.breakdown).toHaveProperty('readability'); + expect(['PASSED', 'NEEDS_IMPROVEMENT', 'INSUFFICIENT_DATA']).toContain(report.status); + }); + + it('perfil vazio recebe score baixo, seções fracas e recomendações', () => { + const report = analyzer.score( + minimalProfile(), + analyzer.analyzeJob({ description: 'React, Node.js' }), + ); + + expect(report.score).toBeLessThan(50); + expect(report.weakSections.length).toBeGreaterThan(0); + expect(report.recommendations.length).toBeGreaterThan(0); + }); + + it('detecta keyword stuffing penalizando a legibilidade', () => { + const stuffed = { + ...fullProfile(), + summary: 'Node Node Node Node Node Node developer', + }; + const normal = analyzer.score(fullProfile(), analyzer.analyzeJob(null)); + const bad = analyzer.score(stuffed, analyzer.analyzeJob(null)); + expect(bad.breakdown.readability).toBeLessThan(normal.breakdown.readability); + }); +}); diff --git a/tests/fixtures.ts b/tests/fixtures.ts new file mode 100644 index 0000000..0d9774e --- /dev/null +++ b/tests/fixtures.ts @@ -0,0 +1,157 @@ +import { NormalizedProfile } from '../src/domain/entities/NormalizedProfile'; + +export function fullProfile(): NormalizedProfile { + return { + name: 'Ana Souza', + headline: 'Engenheira de Software Backend', + summary: 'Engenheira de software com foco em APIs escaláveis.', + contact: { + email: 'ana@example.com', + phone: '+55 11 90000-0000', + location: 'São Paulo, BR', + website: 'ana.dev', + }, + experience: [ + { + company: 'Acme Corp', + role: 'Engenheira de Software', + period: '2022 – Atual', + stack: ['TypeScript', 'Node.js', 'PostgreSQL'], + highlights: [ + 'Desenvolvimento de APIs REST com Node.js e TypeScript', + 'Implementação de pipelines CI/CD', + ], + results: ['Redução de 30% no tempo de resposta'], + source: 'candidate', + }, + ], + education: [ + { + institution: 'USP', + degree: 'Bacharelado', + field: 'Ciência da Computação', + period: '2016 – 2020', + }, + ], + skills: [ + { name: 'TypeScript', category: 'Linguagens', years: 5 }, + { name: 'Node.js', category: 'Backend' }, + { name: 'React', category: 'Frontend' }, + ], + projects: [], + links: [ + { type: 'github', url: 'github.com/ana' }, + { type: 'linkedin', url: 'linkedin.com/in/ana' }, + ], + languages: [{ name: 'Português', level: 'Nativo' }], + }; +} + +export function minimalProfile(): NormalizedProfile { + return { + name: 'João Lima', + contact: {}, + experience: [], + education: [], + skills: [], + projects: [], + links: [], + languages: [], + }; +} + +export function richProfile(): NormalizedProfile { + return { + name: 'Bene Tesla', + headline: 'Engenheiro de Software', + summary: '', + contact: { + email: 'bene@example.com', + phone: '+55 11 90000-0000', + location: 'São Paulo, BR', + website: 'bene.dev', + }, + experience: [ + { + company: 'Acme Corp', + role: 'Engenheiro de Software', + period: '2022 – Atual', + stack: ['TypeScript', 'Node.js', 'PostgreSQL', 'Docker'], + highlights: [ + 'Desenvolvimento de APIs REST com Node.js e TypeScript', + 'Implementação de cache com Redis e otimização de queries', + 'Automação de CI/CD com GitHub Actions', + ], + results: ['Reduziu o tempo de resposta das APIs em 30%'], + source: 'linkedin', + }, + ], + education: [{ institution: 'USP', degree: 'Bacharelado', field: 'Ciência da Computação' }], + skills: [ + { name: 'TypeScript', category: 'Linguagens' }, + { name: 'JavaScript', category: 'Linguagens' }, + { name: 'Java', category: 'Linguagens' }, + { name: 'Node.js', category: 'Backend' }, + { name: 'NestJS', category: 'Backend' }, + { name: 'Spring Boot', category: 'Backend' }, + { name: 'React', category: 'Frontend' }, + { name: 'PostgreSQL', category: 'Bancos de Dados' }, + { name: 'Redis', category: 'Bancos de Dados' }, + { name: 'Docker', category: 'Infraestrutura & DevOps' }, + { name: 'AWS', category: 'Cloud' }, + ], + projects: [ + { + name: 'api-gateway', + description: 'Gateway de APIs em Node.js', + stack: ['Node.js', 'TypeScript', 'Docker'], + highlights: ['Implementou rate limiting e autenticação JWT.', '120 stars'], + source: 'github', + }, + { + name: 'data-pipeline', + description: 'Pipeline de dados com microservices', + stack: ['Java', 'Spring Boot', 'Kafka'], + highlights: ['Projetou microservices com Clean Architecture.'], + source: 'github', + }, + { + name: 'web-dashboard', + description: 'Dashboard em React', + stack: ['React', 'TypeScript'], + highlights: ['Desenvolveu componentes reutilizáveis e testes com Jest.'], + source: 'github', + }, + ], + links: [ + { type: 'github', url: 'https://github.com/bene' }, + { type: 'linkedin', url: 'https://www.linkedin.com/in/bene-tesla' }, + ], + languages: [ + { name: 'Português', level: 'Nativo' }, + { name: 'Inglês', level: 'Avançado' }, + ], + }; +} + +export function projectsOnlyProfile(): NormalizedProfile { + return { + name: 'Maria Dev', + headline: 'Desenvolvedora', + contact: { email: 'maria@example.com' }, + experience: [], + education: [], + skills: [{ name: 'Python' }], + projects: [ + { + name: 'Open Source CLI', + description: 'Ferramenta CLI em Python', + stack: ['Python'], + highlights: ['200 stars no GitHub'], + source: 'github', + }, + ], + links: [], + languages: [], + }; +} diff --git a/tests/githubEnricher.test.ts b/tests/githubEnricher.test.ts new file mode 100644 index 0000000..4175f6c --- /dev/null +++ b/tests/githubEnricher.test.ts @@ -0,0 +1,143 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { + GitHubEnricher, + parseGitHubUsername, +} from '../src/infrastructure/enrichment/GitHubEnricher'; +import { minimalProfile } from './fixtures'; + +const enricher = new GitHubEnricher(); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +function mockFetchOnce(user: unknown, repos: unknown) { + vi.stubGlobal( + 'fetch', + vi.fn((url: string) => { + const u = String(url); + let body: unknown; + if (/\/readme$/.test(u)) { + // README endpoint (profile + repos). Return a small base64 README. + body = { + content: Buffer.from('Projeto em Node.js e TypeScript.').toString('base64'), + encoding: 'base64', + }; + } else if (/\/repos\?/.test(u)) { + body = repos; + } else { + body = user; + } + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve(body), + } as Response); + }), + ); +} + +describe('parseGitHubUsername', () => { + it('extrai o username de uma URL do GitHub', () => { + expect(parseGitHubUsername('https://github.com/bene-tesla')).toBe('bene-tesla'); + }); + it('aceita handle puro', () => { + expect(parseGitHubUsername('@bene-tesla')).toBe('bene-tesla'); + }); + it('retorna null para entrada inválida', () => { + expect(parseGitHubUsername('not a user!!')).toBeNull(); + expect(parseGitHubUsername(undefined)).toBeNull(); + }); +}); + +describe('GitHubEnricher', () => { + it('adiciona o link do LinkedIn (sem scraping)', async () => { + const { profile } = await enricher.enrich(minimalProfile(), { + linkedin: 'https://www.linkedin.com/in/bene-tesla', + }); + expect(profile.links.find((l) => l.type === 'linkedin')?.url).toContain( + 'linkedin.com/in/bene-tesla', + ); + }); + + it('normaliza handle de LinkedIn para URL pública', async () => { + const { profile } = await enricher.enrich(minimalProfile(), { + linkedin: 'bene-tesla', + }); + expect(profile.links.find((l) => l.type === 'linkedin')?.url).toBe( + 'https://www.linkedin.com/in/bene-tesla', + ); + }); + + it('importa repositórios públicos como projetos e linguagens como skills', async () => { + mockFetchOnce( + { + login: 'ana', + name: 'Ana', + bio: 'Dev backend', + blog: 'ana.dev', + location: 'SP', + html_url: 'https://github.com/ana', + }, + [ + { + name: 'cool-api', + description: 'API em Node', + html_url: 'https://github.com/ana/cool-api', + language: 'TypeScript', + stargazers_count: 120, + forks_count: 8, + fork: false, + archived: false, + topics: ['node', 'api'], + }, + { + name: 'fork-repo', + description: 'fork', + html_url: 'x', + language: 'Go', + stargazers_count: 999, + forks_count: 0, + fork: true, + archived: false, + }, + ], + ); + + const { profile, warnings } = await enricher.enrich(minimalProfile(), { + github: 'https://github.com/ana', + }); + + expect(warnings).toEqual([]); + expect(profile.projects.map((p) => p.name)).toContain('cool-api'); + // forks são ignorados + expect(profile.projects.map((p) => p.name)).not.toContain('fork-repo'); + expect(profile.skills.map((s) => s.name)).toContain('TypeScript'); + expect(profile.links.find((l) => l.type === 'github')?.url).toBe( + 'https://github.com/ana', + ); + expect(profile.contact.location).toBe('SP'); + }); + + it('não falha a geração quando o GitHub retorna erro (gera warning)', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(() => + Promise.resolve({ ok: false, status: 404, json: () => Promise.resolve({}) } as Response), + ), + ); + + const { profile, warnings } = await enricher.enrich(minimalProfile(), { + github: 'ghost-user', + }); + + expect(warnings.length).toBe(1); + expect(profile.projects).toEqual([]); + }); + + it('sem sources, retorna o perfil intacto', async () => { + const { profile, warnings } = await enricher.enrich(minimalProfile(), undefined); + expect(warnings).toEqual([]); + expect(profile.links).toEqual([]); + }); +}); diff --git a/tests/http.test.ts b/tests/http.test.ts new file mode 100644 index 0000000..35e4d7a --- /dev/null +++ b/tests/http.test.ts @@ -0,0 +1,66 @@ +import request from 'supertest'; +import { describe, expect, it } from 'vitest'; +import { createServer } from '../src/presentation/http/createServer'; +import { fullProfile } from './fixtures'; + +describe('HTTP /resumes/generate', () => { + it('GET /health retorna ok', async () => { + const app = createServer(); + const res = await request(app).get('/health').expect(200); + expect(res.body.ok).toBe(true); + expect(res.body.service).toBe('ats-forge'); + }); + + it('gera DOCX e expõe o ATS score nos headers', async () => { + const app = createServer(); + const res = await request(app) + .post('/resumes/generate') + .send({ profile: fullProfile(), job: { description: 'Node.js' }, format: 'docx' }) + .expect(200); + + expect(res.headers['content-type']).toContain('wordprocessingml'); + expect(res.headers['content-disposition']).toContain('attachment'); + expect(Number(res.headers['x-ats-score'])).toBeGreaterThanOrEqual(0); + expect(res.headers['x-ats-report']).toBeDefined(); + }); + + it('gera PDF com assinatura válida', async () => { + const app = createServer(); + const res = await request(app) + .post('/resumes/generate') + .send({ profile: fullProfile(), format: 'pdf' }) + .buffer(true) + .parse((r, cb) => { + const chunks: Buffer[] = []; + r.on('data', (c: Buffer) => chunks.push(c)); + r.on('end', () => cb(null, Buffer.concat(chunks))); + }) + .expect(200); + + expect((res.body as Buffer).subarray(0, 5).toString('latin1')).toBe('%PDF-'); + }); + + it('retorna 400 para perfil inválido', async () => { + const app = createServer(); + const res = await request(app) + .post('/resumes/generate') + .send({ profile: { contact: {} }, format: 'docx' }) + .expect(400); + expect(res.body.code).toBe('VALIDATION_ERROR'); + }); + + it('exige API key quando configurada', async () => { + const app = createServer({ apiKey: 'segredo' }); + + await request(app) + .post('/resumes/generate') + .send({ profile: fullProfile(), format: 'docx' }) + .expect(401); + + await request(app) + .post('/resumes/generate') + .set('x-api-key', 'segredo') + .send({ profile: fullProfile(), format: 'docx' }) + .expect(200); + }); +}); diff --git a/tests/profileMapper.test.ts b/tests/profileMapper.test.ts new file mode 100644 index 0000000..46ab0eb --- /dev/null +++ b/tests/profileMapper.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest'; +import { mapProfileToRenderPayload } from '../src/domain/services/ProfileMapper'; +import { fullProfile, minimalProfile, projectsOnlyProfile } from './fixtures'; + +describe('mapProfileToRenderPayload', () => { + it('mapeia contatos, links e formação corretamente', () => { + const payload = mapProfileToRenderPayload(fullProfile(), { title: 'Backend' }, []); + + expect(payload.contact.nome).toBe('Ana Souza'); + expect(payload.contact.github).toBe('github.com/ana'); + expect(payload.contact.linkedin).toBe('linkedin.com/in/ana'); + expect(payload.title).toBe('Backend'); + expect(payload.contact.formacao[0]).toContain('USP'); + }); + + it('agrupa skills por categoria', () => { + const payload = mapProfileToRenderPayload(fullProfile(), null, []); + expect(payload.skills['Linguagens']).toContain('TypeScript'); + expect(payload.skills['Backend']).toContain('Node.js'); + }); + + it('gera resumo a partir de evidências quando summary está ausente', () => { + const payload = mapProfileToRenderPayload(projectsOnlyProfile(), null, []); + expect(payload.profile).toContain('Desenvolvedora'); + }); + + it('coloca projetos na seção de projetos (separada da experiência)', () => { + const payload = mapProfileToRenderPayload(projectsOnlyProfile(), null, []); + expect(payload.experiencias).toEqual([]); + expect(payload.projetos.length).toBe(1); + expect(payload.projetos[0].name).toBe('Open Source CLI'); + expect(payload.projetos[0].description).toBe('Ferramenta CLI em Python'); + }); + + it('não quebra com perfil mínimo', () => { + const payload = mapProfileToRenderPayload(minimalProfile(), null, []); + expect(payload.contact.nome).toBe('João Lima'); + expect(payload.experiencias).toEqual([]); + expect(payload.projetos).toEqual([]); + expect(payload.title).toBe('Profissional'); + }); + + it('remove emoji e entidades HTML do resumo', () => { + const dirty = { + ...fullProfile(), + summary: 'Engenheiro 👋🚀 de software   backend 🧑‍💻', + }; + const payload = mapProfileToRenderPayload(dirty, null, []); + expect(payload.profile).not.toMatch(/[\u{1F000}-\u{1FFFF}]/u); + expect(payload.profile).not.toContain(' '); + expect(payload.profile).toContain('Engenheiro'); + }); +}); diff --git a/tests/renderers.test.ts b/tests/renderers.test.ts new file mode 100644 index 0000000..d9f69d5 --- /dev/null +++ b/tests/renderers.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest'; +import { GenerateResumeFromProfileUseCase } from '../src/application/use-cases/GenerateResumeFromProfileUseCase'; +import { fullProfile, minimalProfile } from './fixtures'; + +const useCase = new GenerateResumeFromProfileUseCase(); + +describe('Resume rendering (end-to-end por formato)', () => { + it('gera DOCX como Buffer não vazio', async () => { + const result = await useCase.execute({ profile: fullProfile(), format: 'docx' }); + expect(Buffer.isBuffer(result.content)).toBe(true); + expect((result.content as Buffer).length).toBeGreaterThan(500); + expect(result.filename).toMatch(/\.docx$/); + expect(result.contentType).toContain('wordprocessingml'); + }); + + it('gera PDF com texto real (assinatura %PDF)', async () => { + const result = await useCase.execute({ profile: fullProfile(), format: 'pdf' }); + const buf = result.content as Buffer; + expect(Buffer.isBuffer(buf)).toBe(true); + expect(buf.subarray(0, 5).toString('latin1')).toBe('%PDF-'); + expect(result.contentType).toBe('application/pdf'); + }); + + it('gera Markdown com as seções esperadas', async () => { + const result = await useCase.execute({ profile: fullProfile(), format: 'md' }); + const md = result.content as string; + expect(typeof md).toBe('string'); + expect(md).toContain('# Ana Souza'); + expect(md).toContain('EXPERIÊNCIA PROFISSIONAL'); + }); + + it('gera currículo mesmo com perfil mínimo', async () => { + const result = await useCase.execute({ profile: minimalProfile(), format: 'pdf' }); + expect((result.content as Buffer).subarray(0, 5).toString('latin1')).toBe('%PDF-'); + expect(result.atsReport.score).toBeGreaterThanOrEqual(0); + }); + + it('inclui o AtsReport no resultado', async () => { + const result = await useCase.execute({ + profile: fullProfile(), + job: { description: 'Node.js, TypeScript' }, + format: 'docx', + }); + expect(result.atsReport.score).toBeGreaterThan(0); + expect(result.atsReport.matchedKeywords.length).toBeGreaterThan(0); + }); + + it('sanitiza o nome do arquivo', async () => { + const result = await useCase.execute({ + profile: { ...fullProfile(), name: 'José / Ação' }, + format: 'md', + }); + expect(result.filename).not.toMatch(/[/\\]/); + }); +}); diff --git a/tests/resumeOptimizer.test.ts b/tests/resumeOptimizer.test.ts new file mode 100644 index 0000000..8e25b2c --- /dev/null +++ b/tests/resumeOptimizer.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; +import { ResumeOptimizer } from '../src/domain/services/ResumeOptimizer'; +import { minimalProfile, richProfile } from './fixtures'; + +describe('ResumeOptimizer', () => { + const optimizer = new ResumeOptimizer(); + + it('atinge ATS Score >= 80 e status PASSED para um perfil rico', () => { + const { report } = optimizer.optimize(richProfile(), { + title: 'Engenheiro Backend', + description: 'Node.js, TypeScript, PostgreSQL, Docker, Redis, microservices', + }); + + expect(report.score).toBeGreaterThanOrEqual(80); + expect(report.status).toBe('PASSED'); + }); + + it('gera um resumo profissional quando ausente (a partir de evidências)', () => { + const { profile } = optimizer.optimize(richProfile(), null); + expect(profile.summary && profile.summary.length).toBeGreaterThan(20); + // deriva de techs reais do perfil + expect(profile.summary).toMatch(/TypeScript|Node\.js|Java/); + }); + + it('normaliza e deduplica tecnologias nas skills', () => { + const base = richProfile(); + base.skills.push({ name: 'nodejs' }, { name: 'TS' }); + const { profile } = optimizer.optimize(base, null); + const names = profile.skills.map((s) => s.name); + expect(names.filter((n) => n === 'Node.js').length).toBe(1); + expect(names).not.toContain('nodejs'); + expect(names).not.toContain('TS'); + }); + + it('não inventa experiência/projetos para um perfil pobre', () => { + const { profile, report } = optimizer.optimize(minimalProfile(), null); + expect(profile.experience).toEqual([]); + expect(profile.projects).toEqual([]); + expect(report.status).toBe('INSUFFICIENT_DATA'); + }); + + it('para de otimizar assim que atinge o threshold', () => { + const { report, iterations } = optimizer.optimize(richProfile(), null); + expect(report.score).toBeGreaterThanOrEqual(80); + expect(iterations).toBeLessThanOrEqual(6); + }); +}); diff --git a/tests/techTaxonomy.test.ts b/tests/techTaxonomy.test.ts new file mode 100644 index 0000000..7b593ed --- /dev/null +++ b/tests/techTaxonomy.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from 'vitest'; +import { + categoryOf, + extractTechnologies, + normalizeTech, + normalizeTechList, +} from '../src/domain/services/TechTaxonomy'; + +describe('TechTaxonomy', () => { + it('normaliza aliases para o nome canônico', () => { + expect(normalizeTech('nodejs')).toBe('Node.js'); + expect(normalizeTech('node')).toBe('Node.js'); + expect(normalizeTech('ts')).toBe('TypeScript'); + expect(normalizeTech('postgres')).toBe('PostgreSQL'); + expect(normalizeTech('k8s')).toBe('Kubernetes'); + }); + + it('remove duplicatas ao normalizar uma lista', () => { + expect(normalizeTechList(['Node', 'node.js', 'NODEJS', 'TypeScript', 'ts'])).toEqual([ + 'Node.js', + 'TypeScript', + ]); + }); + + it('categoriza tecnologias conhecidas', () => { + expect(categoryOf('React')).toBe('Frontend'); + expect(categoryOf('postgres')).toBe('Bancos de Dados'); + expect(categoryOf('docker')).toBe('Infraestrutura & DevOps'); + expect(categoryOf('Clean Architecture')).toBe('Arquitetura & Práticas'); + }); + + it('extrai tecnologias de texto livre (README)', () => { + const text = + 'Backend em Node.js e TypeScript, com PostgreSQL e Redis. Deploy com Docker e AWS. Testes com Jest. Clean Architecture e REST APIs.'; + const techs = extractTechnologies(text); + expect(techs).toEqual( + expect.arrayContaining([ + 'Node.js', + 'TypeScript', + 'PostgreSQL', + 'Redis', + 'Docker', + 'AWS', + 'Jest', + 'Clean Architecture', + 'REST API', + ]), + ); + }); + + it('prefere termos compostos (Spring Boot antes de Spring)', () => { + const techs = extractTechnologies('Experiência com Spring Boot e microservices.'); + expect(techs).toContain('Spring Boot'); + expect(techs).toContain('Microservices'); + }); + + it('não extrai tecnologias ausentes', () => { + expect(extractTechnologies('Trabalho em equipe e boa comunicação.')).toEqual([]); + }); +}); diff --git a/tests/textSanitizer.test.ts b/tests/textSanitizer.test.ts new file mode 100644 index 0000000..6636664 --- /dev/null +++ b/tests/textSanitizer.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import { humanizeName, sanitizeText } from '../src/domain/services/TextSanitizer'; + +describe('sanitizeText', () => { + it('remove emoji e preserva o texto', () => { + expect(sanitizeText('Olá 👋 mundo 🚀')).toBe('Olá mundo'); + }); + + it('decodifica entidades HTML comuns', () => { + expect(sanitizeText('a & b   c')).toBe('a & b c'); + }); + + it('preserva acentos do português', () => { + expect(sanitizeText('Integração e observabilidade em tempo real')).toBe( + 'Integração e observabilidade em tempo real', + ); + }); + + it('normaliza aspas e travessões', () => { + expect(sanitizeText('“aspas” — travessão')).toBe('"aspas" - travessão'); + }); + + it('remove sequências ZWJ de emoji (ex.: 🧑‍💻)', () => { + const out = sanitizeText('Dev 🧑‍💻 pleno'); + expect(out).toBe('Dev pleno'); + }); +}); + +describe('humanizeName', () => { + it('humaniza snake_case e kebab-case', () => { + expect(humanizeName('node-monitoring')).toBe('Node Monitoring'); + expect(humanizeName('CARDS_ICON_VANILLAJS')).toBe('Cards Icon Vanillajs'); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..8996a04 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['tests/**/*.test.ts'], + }, +}); From 414b7d90c30452929d939055087870021be38610 Mon Sep 17 00:00:00 2001 From: Benevanio Date: Wed, 7 Oct 2026 13:43:09 -0300 Subject: [PATCH 2/2] feat: melhoria de busca no filtro. --- .../dtos/GenerateFromProfileInput.ts | 3 + .../GenerateResumeFromProfileUseCase.ts | 101 +++++++++++++++--- src/domain/services/AtsAnalyzer.ts | 18 ++-- src/domain/services/ResumeTailor.ts | 58 ++++++++++ src/domain/services/SummaryWriter.ts | 79 ++++++++++++++ src/index.ts | 9 +- .../enrichment/GitHubEnricher.ts | 13 ++- src/presentation/http/createServer.ts | 6 +- src/presentation/http/resumeController.ts | 42 +++++++- src/presentation/http/validation.ts | 1 + tests/atsAnalyzer.test.ts | 35 +++++- tests/http.test.ts | 24 +++++ tests/resumeTailor.test.ts | 72 +++++++++++++ tests/summaryWriter.test.ts | 68 ++++++++++++ 14 files changed, 498 insertions(+), 31 deletions(-) create mode 100644 src/domain/services/ResumeTailor.ts create mode 100644 src/domain/services/SummaryWriter.ts create mode 100644 tests/resumeTailor.test.ts create mode 100644 tests/summaryWriter.test.ts diff --git a/src/application/dtos/GenerateFromProfileInput.ts b/src/application/dtos/GenerateFromProfileInput.ts index eb7d3a7..17efd95 100644 --- a/src/application/dtos/GenerateFromProfileInput.ts +++ b/src/application/dtos/GenerateFromProfileInput.ts @@ -9,6 +9,9 @@ export interface GenerateFromProfileInput { format: ResumeFormat; /** Public GitHub/LinkedIn links to enrich the profile from. */ sources?: EnrichmentSources | null; + /** Candidate's own "about" text (e.g. the LinkedIn "Sobre"), used as the base + * for a short, job-tailored professional summary. */ + about?: string | null; /** Optional base filename (without extension). Defaults to the candidate name. */ filename?: string; } diff --git a/src/application/use-cases/GenerateResumeFromProfileUseCase.ts b/src/application/use-cases/GenerateResumeFromProfileUseCase.ts index b6f8557..6c62d95 100644 --- a/src/application/use-cases/GenerateResumeFromProfileUseCase.ts +++ b/src/application/use-cases/GenerateResumeFromProfileUseCase.ts @@ -1,52 +1,97 @@ +import { NormalizedProfile } from '../../domain/entities/NormalizedProfile'; import { AtsReport } from '../../domain/services/AtsAnalyzer'; import { mapProfileToRenderPayload } from '../../domain/services/ProfileMapper'; import { ResumeOptimizer } from '../../domain/services/ResumeOptimizer'; +import { ResumeTailor } from '../../domain/services/ResumeTailor'; +import { SummaryWriter } from '../../domain/services/SummaryWriter'; +import { RenderPayload } from '../../domain/interfaces/IResumeRenderer'; import { GitHubEnricher } from '../../infrastructure/enrichment/GitHubEnricher'; import { createRenderer } from '../../infrastructure/renderers/RendererFactory'; import { GenerateFromProfileInput } from '../dtos/GenerateFromProfileInput'; export interface GenerateResumeResult { - /** Rendered document — Buffer for docx/pdf, string for markdown. */ content: Buffer | string; filename: string; contentType: string; extension: string; atsReport: AtsReport; - /** Non-fatal warnings (e.g. a GitHub import that failed). */ warnings: string[]; + sourcesUsed: string[]; +} + +/** Structured, sanitized resume for on-screen preview (ATS-safe, single column). */ +export interface ResumePreview { + name: string; + title: string; + summary: string; + contact: { email: string; phone: string; portfolio: string }; + links: { linkedin: string; github: string }; + skills: Record; + experience: RenderPayload['experiencias']; + projects: RenderPayload['projetos']; + education: string[]; + languages: string[]; +} + +export interface AnalyzeResult { + resume: ResumePreview; + atsReport: AtsReport; + warnings: string[]; + sourcesUsed: string[]; + job: { title: string | null; hasDescription: boolean }; +} + +interface PreparedResume { + profile: NormalizedProfile; + atsReport: AtsReport; + warnings: string[]; + sourcesUsed: string[]; } /** - * Orchestrates resume generation from a normalized profile, with no file I/O: - * analyze the job → score → map profile to payload → render in the chosen - * format. Returns the document in memory so an HTTP layer can stream it. + * Orchestrates resume generation from a normalized profile, with no file I/O in + * the preparation stage: enrich (GitHub/LinkedIn) → optimize (ATS) → tailor to + * the job. `execute` renders a file; `analyze` returns a structured preview. */ export class GenerateResumeFromProfileUseCase { constructor( private readonly optimizer: ResumeOptimizer = new ResumeOptimizer(), + private readonly tailor: ResumeTailor = new ResumeTailor(), private readonly enricher: GitHubEnricher = new GitHubEnricher(), + private readonly summaryWriter: SummaryWriter = new SummaryWriter(), ) {} - async execute(input: GenerateFromProfileInput): Promise { - // 1) Enrich with public GitHub data (profile + repo READMEs) + LinkedIn link. - const { profile, warnings } = await this.enricher.enrich( + private async prepare(input: GenerateFromProfileInput): Promise { + const { profile, warnings, sourcesUsed } = await this.enricher.enrich( input.profile, input.sources ?? undefined, ); - // 2) Iteratively optimize toward the ATS pass threshold (evidence-based only). + // When the candidate provides an "about" (e.g. their LinkedIn "Sobre"), write + // a short, job-tailored summary from it. Otherwise keep what we have and let + // the optimizer generate one from evidence. + const withSummary = + input.about && input.about.trim() + ? { ...profile, summary: this.summaryWriter.write(profile, input.job, input.about) } + : profile; + const { profile: optimized, report: atsReport } = this.optimizer.optimize( - profile, + withSummary, input.job, ); + const tailored = this.tailor.tailor(optimized, input.job); + return { profile: tailored, atsReport, warnings, sourcesUsed }; + } - // 3) Map to the render payload and render in the requested format. - const payload = mapProfileToRenderPayload(optimized, input.job, atsReport.matchedKeywords); + async execute(input: GenerateFromProfileInput): Promise { + const { profile, atsReport, warnings, sourcesUsed } = await this.prepare(input); + const payload = mapProfileToRenderPayload(profile, input.job, atsReport.matchedKeywords); const { renderer, contentType, extension } = createRenderer(input.format); const content = await renderer.render(payload); - - const baseName = sanitizeFilename(input.filename ?? optimized.name ?? 'curriculo'); + // Filename = candidate name + job title (spec: "nome do usuário + nome da vaga"). + const defaultName = [profile.name, input.job?.title].filter((p) => p && p.trim()).join(' '); + const baseName = sanitizeFilename(input.filename ?? (defaultName || 'curriculo')); return { content, @@ -55,6 +100,34 @@ export class GenerateResumeFromProfileUseCase { extension, atsReport, warnings, + sourcesUsed, + }; + } + + async analyze(input: GenerateFromProfileInput): Promise { + const { profile, atsReport, warnings, sourcesUsed } = await this.prepare(input); + const p = mapProfileToRenderPayload(profile, input.job, atsReport.matchedKeywords); + + return { + resume: { + name: p.contact.nome, + title: p.title, + summary: p.profile, + contact: { email: p.contact.email, phone: p.contact.telefone, portfolio: p.contact.portfolio }, + links: { linkedin: p.contact.linkedin, github: p.contact.github }, + skills: p.skills, + experience: p.experiencias, + projects: p.projetos, + education: p.contact.formacao, + languages: p.contact.idiomas, + }, + atsReport, + warnings, + sourcesUsed, + job: { + title: input.job?.title?.trim() || null, + hasDescription: Boolean(input.job?.description?.trim()), + }, }; } } diff --git a/src/domain/services/AtsAnalyzer.ts b/src/domain/services/AtsAnalyzer.ts index 35db90a..d9d7031 100644 --- a/src/domain/services/AtsAnalyzer.ts +++ b/src/domain/services/AtsAnalyzer.ts @@ -175,18 +175,24 @@ export class AtsAnalyzer { evidence: Set, profileTechs: Set, ): { keywords: number; matched: string[]; missing: string[] } { - if (analysis.keywords.length === 0) { - // No job: reward the breadth of the candidate's real keyword footprint. + // Only the job's real technical keywords (Java, Node.js, MuleSoft, …) count — + // never generic words like "desenvolvimento" or "experiência". These are then + // cross-referenced with the candidate's evidence (profile + GitHub + LinkedIn). + const jobTechs = analysis.hardSkills; + + if (jobTechs.length === 0) { + // No recognizable tech requirements: fall back to the breadth of the + // candidate's own keyword footprint, with no noisy matched/missing lists. const count = evidence.size; const value = count >= 45 ? 92 : count >= 30 ? 85 : count >= 18 ? 72 : count >= 8 ? 55 : 32; return { keywords: value, matched: [], missing: [] }; } const isMatch = (k: string) => - evidence.has(k.toLowerCase()) || profileTechs.has(k.toLowerCase()); - const matched = analysis.keywords.filter(isMatch); - const missing = analysis.keywords.filter((k) => !isMatch(k)); - const coverage = Math.round((matched.length / analysis.keywords.length) * 100); + profileTechs.has(k.toLowerCase()) || evidence.has(k.toLowerCase()); + const matched = jobTechs.filter(isMatch); + const missing = jobTechs.filter((k) => !isMatch(k)); + const coverage = Math.round((matched.length / jobTechs.length) * 100); return { keywords: this.clamp(coverage), matched, missing }; } diff --git a/src/domain/services/ResumeTailor.ts b/src/domain/services/ResumeTailor.ts new file mode 100644 index 0000000..750cabd --- /dev/null +++ b/src/domain/services/ResumeTailor.ts @@ -0,0 +1,58 @@ +import { JobTarget, NormalizedProfile } from '../entities/NormalizedProfile'; +import { AtsAnalyzer } from './AtsAnalyzer'; +import { extractTechnologies } from './TechTaxonomy'; + +/** + * Tailors a profile to a specific job by REORDERING (never inventing) its + * content by relevance: job-matched skills first, the most relevant bullets + * first within each experience, and the most job-relevant projects first. + * + * All content is preserved — only the order changes — so the resume stays + * truthful while surfacing what matters for the vacancy (spec §5/§7/§21/§22). + */ +export class ResumeTailor { + constructor(private readonly analyzer: AtsAnalyzer = new AtsAnalyzer()) {} + + tailor(profile: NormalizedProfile, job: JobTarget | null | undefined): NormalizedProfile { + const analysis = this.analyzer.analyzeJob(job); + const jobTerms = new Set( + [...analysis.keywords, ...analysis.hardSkills].map((t) => t.toLowerCase()), + ); + if (jobTerms.size === 0) return profile; + + const score = (text: string): number => { + if (!text) return 0; + const lower = text.toLowerCase(); + const techs = extractTechnologies(text).map((t) => t.toLowerCase()); + let hits = 0; + for (const term of jobTerms) { + if (techs.includes(term) || lower.includes(term)) hits++; + } + return hits; + }; + + // Stable sort helper (preserves original order on ties). + const stableSortDesc = (arr: T[], weight: (item: T) => number): T[] => + arr + .map((item, index) => ({ item, index, w: weight(item) })) + .sort((a, b) => (b.w - a.w) || (a.index - b.index)) + .map((x) => x.item); + + const skills = stableSortDesc(profile.skills, (s) => score(s.name)); + + const experience = profile.experience.map((exp) => ({ + ...exp, + highlights: exp.highlights + ? stableSortDesc(exp.highlights, (h) => score(`${h} ${(exp.stack ?? []).join(' ')}`)) + : exp.highlights, + })); + + const projects = stableSortDesc(profile.projects, (p) => + score( + [p.name, p.description ?? '', ...(p.stack ?? []), ...(p.highlights ?? [])].join(' '), + ), + ); + + return { ...profile, skills, experience, projects }; + } +} diff --git a/src/domain/services/SummaryWriter.ts b/src/domain/services/SummaryWriter.ts new file mode 100644 index 0000000..ed4196e --- /dev/null +++ b/src/domain/services/SummaryWriter.ts @@ -0,0 +1,79 @@ +import { JobTarget, NormalizedProfile } from '../entities/NormalizedProfile'; +import { AtsAnalyzer } from './AtsAnalyzer'; +import { extractTechnologies, normalizeTechList } from './TechTaxonomy'; +import { sanitizeText } from './TextSanitizer'; + +/** + * Writes a SHORT, job-tailored professional summary. + * + * It starts from the candidate's own "about" text (e.g. the LinkedIn "Sobre" + * they pasted) and the job description, and weaves in only the technologies the + * candidate really has that the job asks for. It condenses — it never invents + * (spec §6). + */ + +const MAX_LEN = 320; +const MAX_TECHS = 5; + +export class SummaryWriter { + constructor(private readonly analyzer: AtsAnalyzer = new AtsAnalyzer()) {} + + write( + profile: NormalizedProfile, + job: JobTarget | null | undefined, + about: string | null | undefined, + ): string { + const analysis = this.analyzer.analyzeJob(job); + const profileTechs = this.profileTechnologies(profile); + + // Real + relevant: techs the job asks for AND the candidate actually has. + const relevant = analysis.hardSkills.filter((t) => profileTechs.has(t.toLowerCase())); + const techs = (relevant.length > 0 + ? relevant + : normalizeTechList(profile.skills.map((s) => s.name)) + ).slice(0, MAX_TECHS); + + const role = + sanitizeText(profile.headline) || + sanitizeText(job?.title) || + 'Profissional de tecnologia'; + + const techLine = techs.length > 0 ? `${role} com foco em ${techs.join(', ')}.` : `${role}.`; + + const aboutLine = this.firstSentences(about); + + const summary = [techLine, aboutLine].filter(Boolean).join(' ').trim(); + return this.truncate(summary, MAX_LEN); + } + + /** First 1–2 clean sentences from the candidate's about text. */ + private firstSentences(about: string | null | undefined): string { + const clean = sanitizeText(about).replace(/\s+/g, ' ').trim(); + if (clean.length < 20) return ''; + const sentences = clean.split(/(?<=[.!?])\s+/).slice(0, 2).join(' '); + return this.truncate(sentences, 220); + } + + private truncate(text: string, max: number): string { + if (text.length <= max) return text; + const cut = text.slice(0, max); + const lastSpace = cut.lastIndexOf(' '); + return `${cut.slice(0, lastSpace > 0 ? lastSpace : max).trimEnd()}...`; + } + + private profileTechnologies(profile: NormalizedProfile): Set { + const names = [ + ...profile.skills.map((s) => s.name), + ...profile.experience.flatMap((e) => e.stack ?? []), + ...profile.projects.flatMap((p) => p.stack ?? []), + ...extractTechnologies( + [ + profile.summary ?? '', + ...profile.experience.flatMap((e) => e.highlights ?? []), + ...profile.projects.flatMap((p) => [p.description ?? '', ...(p.highlights ?? [])]), + ].join('\n'), + ), + ]; + return new Set(normalizeTechList(names).map((t) => t.toLowerCase())); + } +} diff --git a/src/index.ts b/src/index.ts index 738b642..73ce8b2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,9 @@ export type { } from './domain/services/AtsAnalyzer'; export { ResumeOptimizer } from './domain/services/ResumeOptimizer'; export type { OptimizationResult } from './domain/services/ResumeOptimizer'; +export { ResumeTailor } from './domain/services/ResumeTailor'; +export { SummaryWriter } from './domain/services/SummaryWriter'; +export { sanitizeText, humanizeName } from './domain/services/TextSanitizer'; export { categoryOf, extractTechnologies, @@ -21,7 +24,11 @@ export { extractKeywordsFromText } from './domain/services/KeywordExtractor'; export { mapProfileToRenderPayload } from './domain/services/ProfileMapper'; export { GenerateResumeFromProfileUseCase } from './application/use-cases/GenerateResumeFromProfileUseCase'; -export type { GenerateResumeResult } from './application/use-cases/GenerateResumeFromProfileUseCase'; +export type { + GenerateResumeResult, + AnalyzeResult, + ResumePreview, +} from './application/use-cases/GenerateResumeFromProfileUseCase'; export type { GenerateFromProfileInput, ResumeFormat, diff --git a/src/infrastructure/enrichment/GitHubEnricher.ts b/src/infrastructure/enrichment/GitHubEnricher.ts index b4029cd..6f61e45 100644 --- a/src/infrastructure/enrichment/GitHubEnricher.ts +++ b/src/infrastructure/enrichment/GitHubEnricher.ts @@ -28,6 +28,8 @@ export interface EnrichmentSources { export interface EnrichmentResult { profile: NormalizedProfile; warnings: string[]; + /** Which external sources actually contributed data (for UI transparency). */ + sourcesUsed: string[]; } interface GitHubUser { @@ -70,13 +72,17 @@ export class GitHubEnricher { sources: EnrichmentSources | undefined, ): Promise { const warnings: string[] = []; + const sourcesUsed = ['candidate']; let enriched = profile; const linkedin = normalizeLinkedIn(sources?.linkedin); - if (linkedin) enriched = addLink(enriched, 'linkedin', linkedin); + if (linkedin) { + enriched = addLink(enriched, 'linkedin', linkedin); + sourcesUsed.push('linkedin'); + } const username = parseGitHubUsername(sources?.github); - if (!username) return { profile: enriched, warnings }; + if (!username) return { profile: enriched, warnings, sourcesUsed }; try { const [user, repos] = await Promise.all([ @@ -100,13 +106,14 @@ export class GitHubEnricher { ]); enriched = this.mergeGitHub(enriched, user, relevant, profileReadme, repoReadmes); + sourcesUsed.push('github'); } catch (err) { warnings.push( `Não foi possível importar o GitHub "${username}": ${(err as Error).message}. O currículo foi gerado com os demais dados.`, ); } - return { profile: enriched, warnings }; + return { profile: enriched, warnings, sourcesUsed }; } private mergeGitHub( diff --git a/src/presentation/http/createServer.ts b/src/presentation/http/createServer.ts index 6c4fdd8..cf80a9c 100644 --- a/src/presentation/http/createServer.ts +++ b/src/presentation/http/createServer.ts @@ -1,7 +1,7 @@ import cors from 'cors'; import express, { NextFunction, Request, Response } from 'express'; import { apiKeyAuth } from './apiKeyAuth'; -import { generateResumeHandler } from './resumeController'; +import { analyzeResumeHandler, generateResumeHandler } from './resumeController'; export interface ServerOptions { apiKey?: string; @@ -30,6 +30,10 @@ export function createServer(options: ServerOptions = {}) { generateResumeHandler(req, res).catch(next); }); + app.post('/resumes/analyze', apiKeyAuth(options.apiKey), (req, res, next) => { + analyzeResumeHandler(req, res).catch(next); + }); + // Fallback 404 app.use((_req: Request, res: Response) => { res.status(404).json({ code: 'NOT_FOUND', message: 'Rota não encontrada.' }); diff --git a/src/presentation/http/resumeController.ts b/src/presentation/http/resumeController.ts index f772411..427860f 100644 --- a/src/presentation/http/resumeController.ts +++ b/src/presentation/http/resumeController.ts @@ -34,11 +34,16 @@ export async function generateResumeHandler(req: Request, res: Response): Promis profile: body.profile, job: body.job ?? null, sources: body.sources ?? null, + about: body.about ?? null, format: body.format, filename: body.filename, }); - const reportJson = JSON.stringify({ ...result.atsReport, warnings: result.warnings }); + const reportJson = JSON.stringify({ + ...result.atsReport, + warnings: result.warnings, + sourcesUsed: result.sourcesUsed, + }); res.setHeader('Content-Type', result.contentType); res.setHeader( 'Content-Disposition', @@ -57,3 +62,38 @@ export async function generateResumeHandler(req: Request, res: Response): Promis res.status(200).send(Buffer.from(result.content, 'utf-8')); } } + +/** + * POST /resumes/analyze + * + * Same inputs as /generate, but returns a JSON preview (structured, sanitized, + * ATS-safe resume) plus the ATS report and the sources used — for on-screen + * preview without downloading a file. + */ +export async function analyzeResumeHandler(req: Request, res: Response): Promise { + let body; + try { + body = generateResumeSchema.parse(req.body); + } catch (err) { + if (err instanceof ZodError) { + res.status(400).json({ + code: 'VALIDATION_ERROR', + message: 'Perfil normalizado inválido.', + details: err.flatten().fieldErrors, + }); + return; + } + throw err; + } + + const result = await useCase.analyze({ + profile: body.profile, + job: body.job ?? null, + sources: body.sources ?? null, + about: body.about ?? null, + format: body.format, + filename: body.filename, + }); + + res.status(200).json(result); +} diff --git a/src/presentation/http/validation.ts b/src/presentation/http/validation.ts index 7504e22..2131295 100644 --- a/src/presentation/http/validation.ts +++ b/src/presentation/http/validation.ts @@ -87,6 +87,7 @@ export const generateResumeSchema = z.object({ profile: normalizedProfileSchema, job: jobTargetSchema.nullish(), sources: sourcesSchema.nullish(), + about: z.string().max(4000).nullish(), format: z.enum(['docx', 'pdf', 'md']).default('docx'), filename: z.string().max(120).optional(), }); diff --git a/tests/atsAnalyzer.test.ts b/tests/atsAnalyzer.test.ts index 7ab7e4c..b4b0a45 100644 --- a/tests/atsAnalyzer.test.ts +++ b/tests/atsAnalyzer.test.ts @@ -18,17 +18,42 @@ describe('AtsAnalyzer v2', () => { expect(analysis.seniority).toBe('Sênior'); }); - it('só conta keywords com evidência no perfil (sem inventar)', () => { + it('conta apenas as TECNOLOGIAS da vaga (não palavras genéricas) e cruza com o perfil', () => { const analysis = analyzer.analyzeJob({ - description: 'Buscamos TypeScript, Node.js, Kubernetes e Docker.', + description: + 'Buscamos profissional com experiência em desenvolvimento usando Java, Node.js, Kubernetes e Docker.', }); const report = analyzer.score(fullProfile(), analysis); + // só techs reais da vaga, em nome canônico expect(report.matchedKeywords).toEqual( - expect.arrayContaining(['typescript', 'node.js']), + expect.arrayContaining(['Node.js']), ); - expect(report.missingKeywords).toEqual(expect.arrayContaining(['kubernetes'])); - expect(report.matchedKeywords).not.toContain('kubernetes'); + expect(report.missingKeywords).toEqual( + expect.arrayContaining(['Kubernetes', 'Docker']), + ); + // palavras genéricas não entram no filtro + expect(report.matchedKeywords).not.toContain('desenvolvimento'); + expect(report.missingKeywords).not.toContain('experiência'); + expect(report.matchedKeywords).not.toContain('Kubernetes'); + }); + + it('junta vaga (java, node, mulesoft) com as techs do perfil (GitHub/LinkedIn)', () => { + const profile = { + ...fullProfile(), + skills: [ + { name: 'Java', category: 'Linguagens' }, + { name: 'Node.js', category: 'Backend' }, + ], + }; + const analysis = analyzer.analyzeJob({ + title: 'Desenvolvedor Backend', + description: 'Java, Node.js e MuleSoft para integrações.', + }); + const report = analyzer.score(profile, analysis); + + expect(report.matchedKeywords.sort()).toEqual(['Java', 'Node.js']); + expect(report.missingKeywords).toEqual(['MuleSoft']); }); it('retorna score, status e breakdown de 6 dimensões', () => { diff --git a/tests/http.test.ts b/tests/http.test.ts index 35e4d7a..460f23f 100644 --- a/tests/http.test.ts +++ b/tests/http.test.ts @@ -49,6 +49,30 @@ describe('HTTP /resumes/generate', () => { expect(res.body.code).toBe('VALIDATION_ERROR'); }); + it('POST /resumes/analyze retorna preview + ats + sourcesUsed', async () => { + const app = createServer(); + const res = await request(app) + .post('/resumes/analyze') + .send({ profile: fullProfile(), job: { title: 'Backend', description: 'node.js typescript' }, format: 'pdf' }) + .expect(200); + + expect(res.body.resume).toBeDefined(); + expect(res.body.resume.name).toBe('Ana Souza'); + expect(res.body.resume.skills).toBeTypeOf('object'); + expect(res.body.atsReport.score).toBeGreaterThanOrEqual(0); + expect(res.body.sourcesUsed).toContain('candidate'); + expect(res.body.job).toEqual({ title: 'Backend', hasDescription: true }); + }); + + it('POST /resumes/analyze valida o corpo (400)', async () => { + const app = createServer(); + const res = await request(app) + .post('/resumes/analyze') + .send({ profile: { contact: {} } }) + .expect(400); + expect(res.body.code).toBe('VALIDATION_ERROR'); + }); + it('exige API key quando configurada', async () => { const app = createServer({ apiKey: 'segredo' }); diff --git a/tests/resumeTailor.test.ts b/tests/resumeTailor.test.ts new file mode 100644 index 0000000..99ac1fe --- /dev/null +++ b/tests/resumeTailor.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest'; +import { NormalizedProfile } from '../src/domain/entities/NormalizedProfile'; +import { ResumeTailor } from '../src/domain/services/ResumeTailor'; + +const tailor = new ResumeTailor(); + +function profile(): NormalizedProfile { + return { + name: 'Dev', + contact: {}, + experience: [ + { + company: 'Acme', + role: 'Dev', + stack: ['Java', 'Spring Boot', 'React'], + highlights: [ + 'Criei telas em React', + 'Desenvolvi APIs REST com Java e Spring Boot', + ], + results: [], + }, + ], + education: [], + skills: [ + { name: 'React' }, + { name: 'Node.js' }, + { name: 'Java' }, + { name: 'Spring Boot' }, + ], + projects: [ + { name: 'frontend-app', stack: ['React'], highlights: [] }, + { name: 'java-api', description: 'API em Java e Spring Boot', stack: ['Java', 'Spring Boot'], highlights: [] }, + ], + links: [], + languages: [], + }; +} + +describe('ResumeTailor', () => { + const job = { title: 'Backend Java', description: 'Java, Spring Boot e REST API' }; + + it('prioriza skills exigidas pela vaga', () => { + const t = tailor.tailor(profile(), job); + const names = t.skills.map((s) => s.name); + expect(names.indexOf('Java')).toBeLessThan(names.indexOf('Node.js')); + expect(names.indexOf('Spring Boot')).toBeLessThan(names.indexOf('React')); + }); + + it('reordena bullets da experiência por relevância à vaga', () => { + const t = tailor.tailor(profile(), job); + expect(t.experience[0].highlights?.[0]).toMatch(/APIs REST com Java/); + }); + + it('prioriza projetos relacionados à vaga', () => { + const t = tailor.tailor(profile(), job); + expect(t.projects[0].name).toBe('java-api'); + }); + + it('não altera a ordem quando não há vaga', () => { + const original = profile(); + const t = tailor.tailor(original, null); + expect(t.skills.map((s) => s.name)).toEqual(original.skills.map((s) => s.name)); + }); + + it('preserva todo o conteúdo (apenas reordena)', () => { + const original = profile(); + const t = tailor.tailor(original, job); + expect(t.skills).toHaveLength(original.skills.length); + expect(t.projects).toHaveLength(original.projects.length); + expect(t.experience[0].highlights).toHaveLength(2); + }); +}); diff --git a/tests/summaryWriter.test.ts b/tests/summaryWriter.test.ts new file mode 100644 index 0000000..202c119 --- /dev/null +++ b/tests/summaryWriter.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from 'vitest'; +import { NormalizedProfile } from '../src/domain/entities/NormalizedProfile'; +import { SummaryWriter } from '../src/domain/services/SummaryWriter'; + +const writer = new SummaryWriter(); + +function profile(): NormalizedProfile { + return { + name: 'Bene', + headline: 'Engenheiro de Software Backend', + contact: {}, + experience: [], + education: [], + skills: [ + { name: 'Java' }, + { name: 'Spring Boot' }, + { name: 'Node.js' }, + { name: 'MuleSoft' }, + { name: 'React' }, + ], + projects: [], + links: [], + languages: [], + }; +} + +describe('SummaryWriter', () => { + const job = { title: 'Backend Java', description: 'Java, Spring Boot, MuleSoft e REST API' }; + + it('gera um resumo curto baseado no "Sobre" do LinkedIn + vaga', () => { + const about = + 'Sou engenheiro de software com paixão por sistemas distribuídos e integrações corporativas de larga escala. Busco sempre qualidade e observabilidade.'; + const summary = writer.write(profile(), job, about); + + expect(summary.length).toBeLessThanOrEqual(320); + expect(summary).toContain('Engenheiro de Software Backend'); + // prioriza techs reais que a vaga pede + expect(summary).toMatch(/Java/); + expect(summary).toMatch(/MuleSoft/); + // inclui a essência do "sobre" + expect(summary).toMatch(/sistemas distribuídos|integrações/i); + // não inclui tech irrelevante à vaga em primeiro plano + expect(summary.startsWith('Engenheiro')).toBe(true); + }); + + it('remove emoji/ruído do about', () => { + const summary = writer.write(profile(), job, 'Dev 🚀🔥 focado em entregas   de valor.'); + expect(summary).not.toMatch(/[\u{1F000}-\u{1FFFF}]/u); + expect(summary).not.toContain(' '); + }); + + it('funciona sem about (usa papel + techs relevantes)', () => { + const summary = writer.write(profile(), job, null); + expect(summary).toContain('Engenheiro de Software Backend'); + expect(summary).toMatch(/Java/); + }); + + it('sem vaga, usa as principais skills do candidato', () => { + const summary = writer.write(profile(), null, 'Desenvolvedor focado em backend.'); + expect(summary).toMatch(/Java|Spring Boot|Node\.js/); + }); + + it('trunca about muito longo', () => { + const long = 'Frase inicial relevante. ' + 'palavra '.repeat(200); + const summary = writer.write(profile(), job, long); + expect(summary.length).toBeLessThanOrEqual(320); + }); +});