build(r0.13): Dockerfiles api/web + compose Dokploy + runbook — R0 prêt à déployer

- image api : multi-stage (pnpm deploy --legacy --prod), client Prisma
  régénéré dans l'arborescence déployée, binaryTargets explicites
  (debian/arm64 openssl-3), entrypoint migrate deploy → seed optionnel
  (SEED_ON_START, compilé dist/seed) → API ; non-root, healthcheck /health
- image web : nginx alpine, statique Vite, proxy /api résolu À LA REQUÊTE
  (resolver Docker + variable — nginx démarre sans l'API), fallback SPA,
  cache immuable /assets, healthcheck IPv4 (127.0.0.1)
- infra/docker-compose.dokploy.yml : 5 services préfixés siop2-, secrets
  exigés, seul siop2-web rejoint dokploy-network (API jamais exposée)
- runbook docs/06-production/runbook-dokploy.md : topologie, profils
  d'environnement, checklist première prod, rollback, répétition locale
- répétition locale validée de bout en bout : migrate+seed au boot,
  parcours demo-login → /users/me à travers nginx conteneurisé, conteneurs
  healthy ; le double verrou ADR-002 (DEMO_MODE sans I_KNOW en production)
  a refusé de démarrer — observé en situation réelle
- prisma passe en dépendance de production (migrations au boot)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-15 22:58:42 +01:00
parent 36de93bb7e
commit ce24d9c32b
13 changed files with 381 additions and 6 deletions

13
.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
**/node_modules
**/dist
**/coverage
**/test-results
**/playwright-report
**/.turbo
.git
.github
.env
**/.env
docs
infra
*.md

View File

@@ -38,5 +38,6 @@ pnpm + Turborepo. `apps/api` : NestJS, Prisma, PostgreSQL (pgvector + PostGIS),
- **R0.10 `apps/api`** : NestJS + Prisma (migration `r0_identity`), guard JWT global fermé par défaut + `@Public()`, PermissionsGuard (matrice en base, cache 60 s), démo-login ADR-002 (module conditionnel, 404 sinon, double verrou prod), seed idempotent (7 rôles, 70 lignes de matrice, 7 comptes démo), `FileStorage`/health ; `packages/shared` (Zod) + `pnpm contract` `docs/openapi.json` committée ; 19 tests Jest verts + smoke test build prod.
- **R0.11 `apps/web`** : React 19 + Vite + Tailwind v4, `tokens.css` copié tel quel + classes extraites de la maquette validée, Manrope auto-hébergée ; connexion + sélecteur démo (masqué si 404 une seule source de vérité : l'API), coquille sidebar (rail safran, écrans futurs marqués R1-R3) / topbar (thème, chip DÉMO, sélecteur de rôle), page /design ; client typé `schema.d.ts` généré depuis `docs/openapi.json` et committé ; vérifié en navigateur réel (bascule de rôle 101 ms, bi-thème, 0 erreur console).
- **R0.12 — tests + CI** (5 jobs verts) : `lint` (ESLint 10 flat config racine, règle ADR-001 anti-import MinIO codée et vérifiée), `ci-contract` (régénération spec+client, diff bloquant), `api` (PostgreSQL 18 + Redis, migrate deploy, Jest couverture 70 % bloquante mesurée 97,5 %), `web` (typecheck + vitest + build), `e2e` (Playwright : parcours démo, bascule < 3 s chronométrée, déconnexion). Badge au README.
- 🔄 **R0 (2/2) — reprise ici** : R0.13 Dockerfiles (api, web) + runbook Dokploy (le déploiement réel attend les accès au serveur du partenaire), puis recette R0 avec le référent (revue pixel écrans maquettes).
- **R0.13 — Dockerfiles + runbook Dokploy** : image api (multi-stage pnpm deploy, `prisma migrate deploy` au boot, seed optionnel `SEED_ON_START`, non-root, healthcheck ; `binaryTargets` explicites) ; image web (nginx, proxy `/api` résolu à la requête, fallback SPA, cache assets) ; `infra/docker-compose.dokploy.yml` (5 services `siop2-`, seul le web sur `dokploy-network`) ; runbook `docs/06-production/runbook-dokploy.md`. **Répétition locale complète validée** (migrate+seed au boot, parcours via nginx conteneurisé, double verrou ADR-002 observé). Le déploiement réel attend les accès au serveur du partenaire.
- 🔄 **R0 — reste** : recette R0 avec le référent (revue pixel écrans maquettes), puis ouverture de R1 Référentiel.
- Détail quotidien : `docs/journal/journal.md`. Dépôt : `siop-spelev/siop2` (privé), jalons R0R5.

39
apps/api/Dockerfile Normal file
View File

@@ -0,0 +1,39 @@
# SIOP V2 — image API (NestJS + Prisma).
# Contexte de build : RACINE du monorepo (docker build -f apps/api/Dockerfile .)
# Étage 1 : dépendances + builds ; étage 2 : runtime minimal (node:24-slim).
FROM node:24-slim AS builder
RUN corepack enable
WORKDIR /repo
# Manifestes d'abord (cache de couche pour pnpm install)
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/shared/package.json packages/shared/package.json
COPY apps/api/package.json apps/api/package.json
RUN pnpm install --frozen-lockfile --filter @siop/api --filter @siop/shared
COPY packages/shared packages/shared
COPY apps/api apps/api
RUN pnpm --filter @siop/shared build \
&& pnpm --filter @siop/api prisma:generate \
&& pnpm --filter @siop/api build \
&& pnpm --filter @siop/api deploy --legacy --prod /out
# Client Prisma regénéré DANS l'arborescence déployée (node_modules à plat)
RUN cd /out && ./node_modules/.bin/prisma generate \
&& cp -r /repo/apps/api/dist /out/dist
FROM node:24-slim
# openssl : requis par les moteurs Prisma
RUN apt-get update \
&& apt-get install -y --no-install-recommends openssl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder --chown=node:node /out /app
COPY --chown=node:node apps/api/docker-entrypoint.sh ./docker-entrypoint.sh
RUN chmod +x docker-entrypoint.sh
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
CMD node -e "fetch('http://localhost:3000/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
ENTRYPOINT ["./docker-entrypoint.sh"]

View File

@@ -0,0 +1,15 @@
#!/bin/sh
# SIOP V2 — démarrage du conteneur API :
# 1. migrations Prisma (idempotentes — la base est toujours à jour du code) ;
# 2. seed optionnel (SEED_ON_START=true : dev/démo — idempotent, jamais
# destructif : il n'écrase ni matrice existante ni mot de passe) ;
# 3. API. Le double verrou DEMO_MODE (ADR-002) est vérifié au boot.
set -e
./node_modules/.bin/prisma migrate deploy
if [ "$SEED_ON_START" = "true" ]; then
node dist/seed/seed.js
fi
exec node dist/main.js

View File

@@ -4,7 +4,7 @@
"private": true,
"description": "SIOP V2 — API NestJS (auth fermée par défaut, matrice de permissions en base, démo-login ADR-002)",
"scripts": {
"build": "nest build",
"build": "nest build && tsc -p tsconfig.seed.json",
"dev": "nest start --watch",
"start": "node dist/main.js",
"typecheck": "tsc -p tsconfig.json --noEmit",
@@ -29,6 +29,7 @@
"dotenv": "^16.4.5",
"ioredis": "^5.4.0",
"minio": "^8.0.0",
"prisma": "^6.8.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"zod": "^4.0.0"
@@ -41,7 +42,6 @@
"@types/node": "^24.0.0",
"@types/supertest": "^6.0.2",
"jest": "^29.7.0",
"prisma": "^6.8.0",
"supertest": "^7.0.0",
"ts-jest": "^29.3.0",
"tsx": "^4.19.0",

View File

@@ -3,6 +3,9 @@
generator client {
provider = "prisma-client-js"
// Cibles explicites : poste de dev (native) + conteneur node:24-slim
// (OpenSSL 3) en x64 (serveur partenaire) et arm64 (répétition locale).
binaryTargets = ["native", "debian-openssl-3.0.x", "linux-arm64-openssl-3.0.x"]
}
datasource db {

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist/seed",
"rootDir": "prisma",
"incremental": false
},
"include": ["prisma/seed.ts"]
}

25
apps/web/Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
# SIOP V2 — image web (React statique derrière nginx).
# Contexte de build : RACINE du monorepo (docker build -f apps/web/Dockerfile .)
FROM node:24-slim AS builder
RUN corepack enable
WORKDIR /repo
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/shared/package.json packages/shared/package.json
COPY apps/web/package.json apps/web/package.json
RUN pnpm install --frozen-lockfile --filter @siop/web --filter @siop/shared
COPY packages/shared packages/shared
COPY apps/web apps/web
RUN pnpm --filter @siop/shared build && pnpm --filter @siop/web build
FROM nginx:1.29-alpine
# Cible du proxy /api — surchargée par le compose (réseau interne)
ENV API_UPSTREAM=siop2-api:3000
COPY apps/web/nginx/default.conf.template /etc/nginx/templates/default.conf.template
COPY --from=builder /repo/apps/web/dist /usr/share/nginx/html
EXPOSE 80
# 127.0.0.1 et non localhost : busybox wget tente ::1 alors que nginx écoute en IPv4
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -q -O /dev/null http://127.0.0.1/ || exit 1

View File

@@ -0,0 +1,36 @@
# SIOP V2 — nginx du conteneur web : statique + proxy /api → API interne.
# ${API_UPSTREAM} est substitué au démarrage (envsubst intégré à l'image nginx) ;
# même topologie que le proxy Vite en dev : /api est RETIRÉ avant transmission.
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# DNS Docker interrogé À LA REQUÊTE (et pas une fois au boot) : nginx
# démarre et sert le statique même si l'API n'est pas encore résolvable.
resolver 127.0.0.11 valid=10s ipv6=off;
set $api_upstream http://${API_UPSTREAM};
location /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass $api_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Assets fingerprintés par Vite : cache long immuable
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
# SPA : toute route applicative sert index.html (jamais mis en cache)
location / {
add_header Cache-Control "no-cache";
try_files $uri /index.html;
}
}

View File

@@ -0,0 +1,98 @@
# Runbook — déploiement Dokploy
> **Rôle de ce document (playbook)** : la procédure REJOUABLE de mise en production
> sur Dokploy (PaaS auto-hébergé du partenaire), écrite en R0.13 et répétée à chaque
> release (principe « déployer tôt »). Statut : **répété en local, en attente des
> accès au serveur du partenaire** pour la première exécution réelle.
## 1. Topologie
Un projet Dokploy = un service « Compose » pointant sur ce dépôt,
fichier [`infra/docker-compose.dokploy.yml`](../../infra/docker-compose.dokploy.yml) :
```
domaine (Traefik/Dokploy) ──▶ siop2-web :80 (nginx, statique)
│ /api/** (préfixe retiré)
siop2-api :3000 (NestJS)
│── siop2-postgres :5432 (pgvector + PostGIS)
│── siop2-redis :6379
└── siop2-minio :9000
```
- **Convention `siop2-`** (leçon v1) : le réseau Dokploy est partagé entre projets ;
un service nommé `postgres` ou `api` collisionne. Tout est préfixé, sans exception.
- Seul `siop2-web` rejoint `dokploy-network` (réseau externe du reverse-proxy) :
l'API n'est **jamais** exposée directement, elle est servie via le proxy `/api` de nginx
— même topologie que le proxy Vite en dev, donc mêmes chemins partout.
- Les migrations Prisma s'appliquent **au démarrage du conteneur API**
(`prisma migrate deploy`, idempotent) : la base est toujours au niveau du code déployé.
## 2. Variables d'environnement (à saisir dans Dokploy, jamais dans le dépôt)
| Variable | Obligatoire | Rôle |
| --- | --- | --- |
| `POSTGRES_PASSWORD` | ✅ | mot de passe PostgreSQL (générer : `openssl rand -hex 24`) |
| `MINIO_ROOT_PASSWORD` | ✅ | secret MinIO |
| `JWT_SECRET` | ✅ | signature des jetons (générer : `openssl rand -hex 32`) |
| `POSTGRES_USER` / `POSTGRES_DB` | — | défaut `siop` / `siop` |
| `DEMO_MODE` | — | **absent en production client.** `true` uniquement sur l'instance de démonstration |
| `DEMO_MODE_I_KNOW` | — | double verrou ADR-002 : requis si `DEMO_MODE=true` en production, sinon l'API **refuse de démarrer** |
| `SEED_ON_START` | — | `true` sur l'instance de démonstration : rôles, matrice et comptes démo au boot (idempotent) |
| `SEED_DEMO_PASSWORD` | — | mot de passe commun des comptes démo (défaut `Demo!2026`) |
Profils types :
- **Production client** : les 4 dernières variables **absentes**. Les routes
`/auth/demo-accounts` et `/auth/demo-login` n'existent pas (404, testé en CI).
- **Instance de démonstration** : `DEMO_MODE=true`, `DEMO_MODE_I_KNOW=true`,
`SEED_ON_START=true`.
## 3. Première mise en production (checklist)
1. Dokploy → **Create Project** `siop2`**Compose** ; source = dépôt
`siop-spelev/siop2`, branche `main`, fichier `infra/docker-compose.dokploy.yml`.
2. Renseigner les variables d'environnement (§2) dans l'onglet Environment.
3. **Deploy**. Dokploy construit les images (contexte = racine du dépôt,
Dockerfiles `apps/api` et `apps/web`) puis démarre les 5 services ;
l'API attend PostgreSQL/Redis/MinIO sains (healthchecks) et migre la base.
4. Onglet Domains du service `siop2-web` : associer le domaine (port **80**,
HTTPS Let's Encrypt géré par Dokploy).
5. Vérifications :
- `https://<domaine>/api/health``{"status":"ok", ...}` (les 3 services `up`) ;
- production client : `https://<domaine>/api/auth/demo-accounts`**404** ;
- instance démo : écran de connexion avec les 7 comptes, bascule de rôle < 3 s.
6. Consigner la mise en production dans `docs/journal/journal.md` (date, version, domaine).
## 4. Releases suivantes
`git push` sur `main` (CI verte exigée) Dokploy **Deploy** (ou webhook auto-deploy).
Les migrations de la release s'appliquent au démarrage ; en cas d'échec de migration,
le conteneur s'arrête **sans** servir de trafic (l'ancienne version reste visible côté web).
## 5. Incidents & retours arrière
- **Rollback applicatif** : Dokploy Deployments redéployer le commit précédent.
Les migrations Prisma étant additives (convention projet : jamais de `DROP` sans
tâche dédiée validée), l'ancienne API fonctionne sur le schéma plus récent.
- **API en échec au boot** : `docker logs siop2-api`. Causes classiques :
`DEMO_MODE=true` sans `DEMO_MODE_I_KNOW` (verrou ADR-002 c'est voulu),
`DATABASE_URL` erronée, migration échouée.
- **Sauvegardes** : volumes `pg-data` (base) et `minio-data` (fichiers).
Sauvegarde PostgreSQL planifiée côté Dokploy (onglet Backups) à configurer
à la première mise en production réelle.
## 6. Répétition locale (sans Dokploy)
```bash
# depuis la racine — construit et lance les 5 services comme en production
docker network create dokploy-network 2>/dev/null || true
POSTGRES_PASSWORD=repetition MINIO_ROOT_PASSWORD=repetition-minio \
JWT_SECRET=$(openssl rand -hex 32) DEMO_MODE=true DEMO_MODE_I_KNOW=true SEED_ON_START=true \
docker compose -f infra/docker-compose.dokploy.yml up -d --build
# web : http://localhost via un port publié à ajouter ponctuellement, ou :
docker compose -f infra/docker-compose.dokploy.yml exec siop2-web wget -qO- http://localhost/api/health
```
C'est cette répétition (images construites, migrations au boot, parcours démo via
nginx) qui a validé R0.13 en local voir le journal du 15/07/2026.

View File

@@ -4,6 +4,32 @@ Trace chronologique des sessions (la plus récente en premier). Le **playbook**
---
## 2026-07-15 — Pr. Daaif (+ Claude) — R0.13 : Dockerfiles + runbook Dokploy
**Actions**
- **Image API** (multi-stage, node:24-slim) : `pnpm deploy --legacy --prod`, client Prisma régénéré dans l'arborescence déployée, entrypoint `prisma migrate deploy` → seed optionnel (`SEED_ON_START=true`, compilé en `dist/seed`) → API ; utilisateur non-root, HEALTHCHECK `/health`. `prisma` passe en dépendance de production (migrations au boot).
- **Image web** (nginx:alpine) : statique Vite + proxy `/api``${API_UPSTREAM}` (template envsubst) — même topologie que le proxy Vite de dev ; cache immuable sur `/assets`, `no-cache` sur `index.html`.
- **`infra/docker-compose.dokploy.yml`** : 5 services préfixés `siop2-`, secrets exigés (`:?`), profils production client / instance démo documentés dans le fichier ; seul `siop2-web` rejoint `dokploy-network` (l'API n'est jamais exposée).
- **Runbook** `docs/06-production/runbook-dokploy.md` : topologie, variables, checklist de première mise en production, releases suivantes, rollback, répétition locale.
- **Répétition locale validée** : images construites, conteneurs lancés sur le réseau infra (migrate + seed au boot), parcours complet vérifié à travers nginx conteneurisé (`/api/health` tout `up`, demo-login → `/users/me`, fallback SPA).
**Leçons de la répétition (consignées pour le playbook)**
- **Prisma en conteneur** : `binaryTargets` explicites dans `schema.prisma` (`debian-openssl-3.0.x` x64 serveur + `linux-arm64-openssl-3.0.x` répétition Mac) — sinon mismatch de moteur au runtime.
- **nginx** : upstream résolu **à la requête** (resolver `127.0.0.11` + variable) et non au boot — sinon nginx refuse de démarrer si l'API n'est pas encore là.
- **Healthcheck alpine** : `127.0.0.1` et non `localhost` (busybox wget tente ::1, nginx écoute en IPv4).
- **Le double verrou ADR-002 a été prouvé en vraie situation** : l'image (NODE_ENV=production) avec `DEMO_MODE=true` sans `DEMO_MODE_I_KNOW` **refuse de démarrer** — comportement observé, pas seulement testé.
**Décisions**
- Migrations **au démarrage du conteneur** (idempotentes) : la base suit toujours le code déployé ; un échec de migration arrête l'API sans servir de trafic.
- Le web est l'unique service exposé (domaine → nginx → proxy interne `/api`) : mêmes chemins en dev et en prod, surface d'attaque minimale.
**Statut** : R0.13 prêt — la première exécution réelle attend les **accès au serveur du partenaire**. Reste pour clore R0 : recette avec le référent (revue pixel écrans ↔ maquettes).
---
## 2026-07-15 — Pr. Daaif (+ Claude) — R0.12 (2/2) : ESLint + parcours Playwright — R0.12 CLOS
**Actions**

View File

@@ -0,0 +1,110 @@
# SIOP V2 — compose de PRODUCTION (Dokploy). Voir docs/06-production/runbook-dokploy.md.
# CONVENTION (leçon v1) : tout service/conteneur est préfixé « siop2- » — le
# réseau Dokploy est partagé entre projets, un nom générique collisionne.
# Les secrets (${...}) vivent dans l'environnement du projet Dokploy, jamais ici.
name: siop2
services:
siop2-postgres:
container_name: siop2-postgres
build: ./postgres
image: siop2/postgres:18-pgvector-postgis
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-siop}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?définir POSTGRES_PASSWORD dans Dokploy}
POSTGRES_DB: ${POSTGRES_DB:-siop}
volumes:
- pg-data:/var/lib/postgresql
- ./postgres/init.sql:/docker-entrypoint-initdb.d/10-extensions.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-siop} -d ${POSTGRES_DB:-siop}"]
interval: 5s
timeout: 3s
retries: 10
siop2-redis:
container_name: siop2-redis
image: redis:7.4.9-alpine
restart: unless-stopped
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
siop2-minio:
container_name: siop2-minio
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-siop}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?définir MINIO_ROOT_PASSWORD dans Dokploy}
volumes:
- minio-data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 5s
timeout: 3s
retries: 10
siop2-api:
container_name: siop2-api
build:
context: ..
dockerfile: apps/api/Dockerfile
image: siop2/api:latest
restart: unless-stopped
environment:
NODE_ENV: production
PORT: 3000
DATABASE_URL: postgresql://${POSTGRES_USER:-siop}:${POSTGRES_PASSWORD}@siop2-postgres:5432/${POSTGRES_DB:-siop}
REDIS_URL: redis://siop2-redis:6379
JWT_SECRET: ${JWT_SECRET:?définir JWT_SECRET dans Dokploy}
MINIO_ENDPOINT: siop2-minio
MINIO_PORT: 9000
MINIO_ACCESS_KEY: ${MINIO_ROOT_USER:-siop}
MINIO_SECRET_KEY: ${MINIO_ROOT_PASSWORD}
# ADR-002 — production client : les 3 variables restent ABSENTES.
# Instance de démonstration publique UNIQUEMENT :
# DEMO_MODE=true + DEMO_MODE_I_KNOW=true (double verrou) + SEED_ON_START=true
DEMO_MODE: ${DEMO_MODE:-}
DEMO_MODE_I_KNOW: ${DEMO_MODE_I_KNOW:-}
SEED_ON_START: ${SEED_ON_START:-}
SEED_DEMO_PASSWORD: ${SEED_DEMO_PASSWORD:-}
depends_on:
siop2-postgres:
condition: service_healthy
siop2-redis:
condition: service_healthy
siop2-minio:
condition: service_healthy
siop2-web:
container_name: siop2-web
build:
context: ..
dockerfile: apps/web/Dockerfile
image: siop2/web:latest
restart: unless-stopped
environment:
API_UPSTREAM: siop2-api:3000
depends_on:
- siop2-api
networks:
- default
- dokploy-network
volumes:
pg-data:
redis-data:
minio-data:
networks:
# Réseau du reverse-proxy Dokploy (Traefik) : seul siop2-web y est exposé,
# le domaine pointe sur lui (port 80) ; l'API reste interne (proxy /api).
dokploy-network:
external: true

6
pnpm-lock.yaml generated
View File

@@ -56,6 +56,9 @@ importers:
minio:
specifier: ^8.0.0
version: 8.0.7
prisma:
specifier: ^6.8.0
version: 6.19.3(typescript@5.9.3)
reflect-metadata:
specifier: ^0.2.2
version: 0.2.2
@@ -87,9 +90,6 @@ importers:
jest:
specifier: ^29.7.0
version: 29.7.0(@types/node@24.13.3)
prisma:
specifier: ^6.8.0
version: 6.19.3(typescript@5.9.3)
supertest:
specifier: ^7.0.0
version: 7.2.2(supports-color@10.2.2)