@bi-analytics/arness-back 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +156 -2
  3. package/bin/arness.mjs +710 -0
  4. package/harness/AGENTS.md +238 -0
  5. package/harness/agents/implementador.md +42 -0
  6. package/harness/agents/lider.md +72 -0
  7. package/harness/agents/revisor.md +62 -0
  8. package/harness/decisiones/README.md +17 -0
  9. package/harness/decisiones/_plantilla.md +24 -0
  10. package/harness/init.sh +743 -0
  11. package/harness/playbooks/add-database.md +16 -0
  12. package/harness/playbooks/new-endpoint.md +88 -0
  13. package/harness/playbooks/new-module.md +21 -0
  14. package/harness/playbooks/new-monitor-widget.md +31 -0
  15. package/harness/playbooks/review.md +41 -0
  16. package/harness/playbooks/update-node-lts.md +46 -0
  17. package/harness/reference/configuracion.md +111 -0
  18. package/harness/reference/contrato-http.md +169 -0
  19. package/harness/reference/novedades.json +158 -0
  20. package/harness/reference/progress-plantilla.md +50 -0
  21. package/harness/skills/commit-hygiene/SKILL.md +137 -0
  22. package/harness/skills/nest-base/SKILL.md +168 -0
  23. package/harness/skills/nest-base/templates/config/auth.spec.ts +32 -0
  24. package/harness/skills/nest-base/templates/config/auth.ts +80 -0
  25. package/harness/skills/nest-base/templates/config/configuration.spec.ts +85 -0
  26. package/harness/skills/nest-base/templates/config/configuration.ts +137 -0
  27. package/harness/skills/nest-base/templates/cors/cors.spec.ts +53 -0
  28. package/harness/skills/nest-base/templates/cors/cors.ts +75 -0
  29. package/harness/skills/nest-base/templates/docs/EVOLUTIVO.md +27 -0
  30. package/harness/skills/nest-base/templates/docs/PENDIENTES.md +20 -0
  31. package/harness/skills/nest-base/templates/docs/TECNICO.md +43 -0
  32. package/harness/skills/nest-base/templates/e2e/cors.e2e-spec.ts +60 -0
  33. package/harness/skills/nest-base/templates/e2e/monitor.e2e-spec.ts +151 -0
  34. package/harness/skills/nest-base/templates/editorconfig +12 -0
  35. package/harness/skills/nest-base/templates/eslint.boundaries.mjs +65 -0
  36. package/harness/skills/nest-base/templates/eslint.config.mjs +82 -0
  37. package/harness/skills/nest-base/templates/logging/logger-options.spec.ts +57 -0
  38. package/harness/skills/nest-base/templates/logging/logger-options.ts +85 -0
  39. package/harness/skills/nest-base/templates/monitor/monitor-login-redirect.filter.ts +16 -0
  40. package/harness/skills/nest-base/templates/monitor/monitor-login.dto.ts +8 -0
  41. package/harness/skills/nest-base/templates/monitor/monitor-page.spec.ts +41 -0
  42. package/harness/skills/nest-base/templates/monitor/monitor-page.ts +190 -0
  43. package/harness/skills/nest-base/templates/monitor/monitor-session.guard.spec.ts +46 -0
  44. package/harness/skills/nest-base/templates/monitor/monitor-session.guard.ts +36 -0
  45. package/harness/skills/nest-base/templates/monitor/monitor-session.service.spec.ts +57 -0
  46. package/harness/skills/nest-base/templates/monitor/monitor-session.service.ts +77 -0
  47. package/harness/skills/nest-base/templates/monitor/monitor-widget.interface.ts +18 -0
  48. package/harness/skills/nest-base/templates/monitor/monitor.constants.ts +25 -0
  49. package/harness/skills/nest-base/templates/monitor/monitor.controller.ts +84 -0
  50. package/harness/skills/nest-base/templates/monitor/monitor.decorators.ts +11 -0
  51. package/harness/skills/nest-base/templates/monitor/monitor.module.ts +30 -0
  52. package/harness/skills/nest-base/templates/monitor/monitor.service.spec.ts +55 -0
  53. package/harness/skills/nest-base/templates/monitor/monitor.service.ts +49 -0
  54. package/harness/skills/nest-base/templates/monitor/widgets/endpoints-total.widget.spec.ts +51 -0
  55. package/harness/skills/nest-base/templates/monitor/widgets/endpoints-total.widget.ts +53 -0
  56. package/harness/skills/nest-base/templates/prettierignore +6 -0
  57. package/harness/skills/nest-base/templates/prettierrc.json +8 -0
  58. package/harness/skills/nest-base/templates/security/login-attempts.spec.ts +48 -0
  59. package/harness/skills/nest-base/templates/security/login-attempts.ts +90 -0
  60. package/harness/skills/nest-base/templates/security/security-headers.spec.ts +27 -0
  61. package/harness/skills/nest-base/templates/security/security-headers.ts +21 -0
  62. package/harness/skills/nest-base/templates/security/security.module.ts +20 -0
  63. package/harness/skills/nest-base/templates/security/session-lifetime.spec.ts +21 -0
  64. package/harness/skills/nest-base/templates/security/session-lifetime.ts +28 -0
  65. package/package.json +44 -3
@@ -0,0 +1,16 @@
1
+ # Playbook: agregar una conexión de BD
2
+
3
+ Referencia: `AGENTS.md` §6 y §13.
4
+
5
+ 1. **Confirmar con el humano:** el nombre lógico de la conexión (UPPER_SNAKE, ej. `REPORTS`) y qué módulos la usarán. Nunca pidas ni escribas credenciales reales en el código ni en el chat.
6
+ 2. Agrega el nombre a `DATABASES` y el bloque de variables `DB_<NOMBRE>_*` en:
7
+ - `.env.example`, con valores de ejemplo;
8
+ - `.env.local`, con valores locales de desarrollo.
9
+ - **No toques `.env`.**
10
+ 3. Verifica que la validación de `src/config/` exige las variables de cada nombre listado en `DATABASES`, y que `parseDatabases` de `src/config/configuration.ts` arma su bloque en `AppConfig.databases`. Normalmente no requiere cambios de código, porque ambas son dinámicas. Nunca leas `DB_<NOMBRE>_*` con `process.env` fuera de `src/config/`.
11
+ 4. En los repositories que la usen, inyecta el pool con `@Inject(getPoolToken('<NOMBRE>'))`.
12
+ 5. Confirma que `/health` reporta la nueva conexión.
13
+ 6. Documenta la conexión en el `README.md` de cada módulo que la use y en la tabla de conexiones de `docs/TECNICO.md` (solo nombre lógico y origen, nunca credenciales), y agrega una entrada en `docs/EVOLUTIVO.md`.
14
+ 7. Corre `./harness/init.sh`.
15
+
16
+ Recuerda que el usuario de BD debe ser de solo lectura. El pool fuerza `default_transaction_read_only = on` como segunda barrera, no como única.
@@ -0,0 +1,88 @@
1
+ # Playbook: crear un endpoint (reporte)
2
+
3
+ Referencia de reglas: `AGENTS.md` §5 (patrón), §6 (BD), §7 (caché), §9 (contrato), §12 (documentación).
4
+
5
+ ## 0. Confirmar el contrato (antes de escribir código)
6
+
7
+ Presenta esto al humano y espera su confirmación:
8
+
9
+ ```
10
+ Endpoint: GET /api/v1/<modulo>/<segmento>
11
+ Ubicación: src/modules/<cliente>/<modulo>/<endpoint>/
12
+ Conexión BD: <NOMBRE> Origen: <schema.tabla | vista>
13
+ Columnas: <col1: tipo>, <col2: tipo>, ...
14
+ Filtros: <nombre: tipo, obligatorio|opcional, default>
15
+ Paginación: sí|no (pageSize default N, máx M)
16
+ Orden: <columnas permitidas>, default <col DESC>
17
+ Caché TTL: <segundos>
18
+ ```
19
+
20
+ Si falta información, pregunta. No inventes columnas, tablas ni reglas.
21
+
22
+ ## 1. Archivos, en este orden
23
+
24
+ 1. `<endpoint>.columns.ts`: `export const X_COLUMNS = Object.freeze([...] as const)`.
25
+ 2. `<endpoint>.entity.ts`: el tipo de la fila, derivado de las columnas.
26
+ 3. `<endpoint>-query.dto.ts`: los params HTTP con `class-validator` (`@IsOptional`, `@IsDateString`, `@IsInt`, `@Min`, `@Max`, `@IsIn`).
27
+ 4. `<endpoint>-filters.dto.ts`: los filtros normalizados (fechas como `Date`, defaults aplicados, `page`/`pageSize` resueltos).
28
+ 5. `<endpoint>.sql.ts`: funciones puras `buildXQuery(filters): { text: string; values: unknown[] }` y, si pagina, `buildXCountQuery(filters)`. Todo valor va como `$n`, y el ORDER BY sale de una allowlist de `columns.ts`.
29
+ 6. `<endpoint>.repository.ts`: el token `X_REPOSITORY`, la interfaz `XRepository` y la clase `PgXRepository`, que inyecta el pool de la conexión nombrada.
30
+ 7. `<endpoint>.service.ts`: Cache-Aside con `CacheService`, y retorna `{ data, meta? }` junto al estado de caché.
31
+ 8. `<endpoint>.controller.ts`: `@Get('<segmento>')`, recibe el DTO, lo convierte en filtros, llama al service y fija `X-Cache`.
32
+ 9. Registrar controller, service y provider del repository (`{ provide: X_REPOSITORY, useClass: PgXRepository }`) en `<modulo>.module.ts`.
33
+
34
+ Aplica la regla de agrupación (más de 2 archivos del mismo tipo en una carpeta → carpeta por tipo). `init.sh` verifica que el endpoint tenga todos estos archivos y los specs de la sección 2.
35
+
36
+ ## 2. Tests
37
+
38
+ - `*.sql.spec.ts`: valida `text` y `values` para cada combinación de filtros, y que ningún valor aparezca interpolado en `text`.
39
+ - `*.service.spec.ts`: HIT, MISS, BYPASS (caché lanza error o está deshabilitada), con y sin paginación.
40
+ - `*.controller.spec.ts`: delega al service y fija `X-Cache`.
41
+ - `*.repository.spec.ts`: con pool mockeado, verifica que ejecuta el SQL del builder y mapea las filas.
42
+ - `*-query.dto.spec.ts`: casos válidos e inválidos.
43
+ - E2E en `test/`: 401 sin token, 200 con token y envelope correcto, 400 con filtro inválido.
44
+
45
+ ## 3. Documentación
46
+
47
+ ### Postman
48
+
49
+ Agrega la request en `postman/arness-back.postman_collection.json`, dentro de la carpeta `<cliente>/<modulo>`:
50
+ - `GET {{baseUrl}}/api/v1/<modulo>/<segmento>?param=ejemplo`
51
+ - Auth: heredada de la colección (`Bearer {{accessToken}}`)
52
+ - Sin descripciones largas.
53
+
54
+ ### FRONT.md
55
+
56
+ Agrega una sección con esta plantilla:
57
+
58
+ ````markdown
59
+ ### <Nombre legible del reporte>
60
+
61
+ `GET /api/v1/<modulo>/<segmento>` · requiere `Authorization: Bearer <accessToken>`
62
+
63
+ Qué devuelve: <una frase>.
64
+
65
+ | Parámetro | Tipo | Obligatorio | Ejemplo |
66
+ |---|---|---|---|
67
+ | desde | fecha ISO | sí | 2026-01-01 |
68
+ | page | número | no (default 1) | 1 |
69
+
70
+ ```json
71
+ { "data": [{ "col1": "…", "col2": 0 }], "meta": { "pagination": { "page": 1, "pageSize": 50, "total": 120, "totalPages": 3 } } }
72
+ ```
73
+
74
+ **Consumo recomendado:** <paginar con pageSize ≤ N | traer completo>; <filtros a enviar siempre>; ante `401 TOKEN_EXPIRED`, llamar a `/auth/refresh` y reintentar una vez; datos cacheados en servidor por <TTL>, así que no hace falta hacer polling más frecuente.
75
+ ````
76
+
77
+ ### README del módulo
78
+
79
+ Agrega el endpoint, su conexión y el origen de datos.
80
+
81
+ ### docs/
82
+
83
+ - `docs/EVOLUTIVO.md`: agrega al final una entrada `## <AAAA-MM-DD HH:mm -0500> · <cliente>/<modulo>` (hora de Bogotá) con el responsable (`git config user.name`), lo entregado en lenguaje de negocio (qué reporte quedó disponible), el endpoint y la referencia. Solo avance.
84
+ - `docs/PENDIENTES.md`: lo que quedó abierto (por ejemplo, una vista que aún no existe en la BD o un filtro aplazado).
85
+
86
+ ## 4. Cierre
87
+
88
+ Corre `./harness/init.sh` y revisa el Definition of Done de `AGENTS.md` §15.
@@ -0,0 +1,21 @@
1
+ # Playbook: crear un módulo o cliente
2
+
3
+ Referencia: `AGENTS.md` §4 y §5.
4
+
5
+ 1. **Confirmar con el humano:** nombre del cliente (carpeta de primer nivel), nombre del módulo, segmento base de la URL y conexión de BD por defecto.
6
+ 2. **Cliente nuevo:** crea `src/modules/<cliente>/`. Si se quiere agrupar sus módulos, agrega `<cliente>.module.ts` que los importe, y regístralo en `app.module.ts`.
7
+ 3. **Módulo nuevo:** crea `src/modules/<cliente>/<modulo>/` con:
8
+ - `<modulo>.module.ts`: `@Module({ controllers: [], providers: [] })`, sin imports de otros módulos de negocio. El código del módulo solo importa de su propia carpeta, de `src/common/` y de `src/config/` (ESLint lo verifica).
9
+ - `README.md` con esta plantilla:
10
+ ```markdown
11
+ # <Módulo>
12
+ Propósito: <una frase>.
13
+ Conexión BD: <NOMBRE>.
14
+ | Endpoint | Origen de datos | Caché TTL |
15
+ |---|---|---|
16
+ ```
17
+ 4. Registra el módulo en su padre (el módulo del cliente o `app.module.ts`).
18
+ 5. Crea la carpeta `<cliente>/<modulo>` en la colección de Postman y un encabezado `## <Módulo>` en `FRONT.md`.
19
+ 6. Agrega endpoints con `new-endpoint.md`.
20
+ 7. Agrega el módulo a la tabla de módulos de `docs/TECNICO.md` y una entrada en `docs/EVOLUTIVO.md` (solo avance; lo abierto va a `docs/PENDIENTES.md`).
21
+ 8. Corre `./harness/init.sh`.
@@ -0,0 +1,31 @@
1
+ # Playbook: agregar un widget al monitor
2
+
3
+ Referencia: `AGENTS.md` §10 y §13, y `harness/reference/contrato-http.md` (Monitor). El monitor ya existe en `src/common/monitor/` (skill `nest-base`).
4
+
5
+ ## 0. Confirmar con el humano
6
+
7
+ ```
8
+ Widget: <id-kebab> · Título: <texto corto> · Orden: <número>
9
+ Muestra: <qué cifra o estado técnico> · Desglose: <sí/no, por qué>
10
+ Origen: <de dónde sale el dato: Nest, pool, caché, process>
11
+ Costo: <consultas o llamadas por carga de página>
12
+ ```
13
+
14
+ **Qué puede mostrar:** conteos, estados (`up`/`down`), versiones, uptime, tamaños de pool y latencias agregadas.
15
+ **Qué nunca muestra:** secretos o variables de entorno, SQL, mensajes de error de PostgreSQL o Redis, hosts o usuarios de BD, y datos de negocio (filas, montos, nombres de clientes). Si el humano pide algo de esta lista, detente: requiere una decisión en `harness/decisiones/`.
16
+
17
+ ## 1. Implementar
18
+
19
+ 1. Crea `src/common/monitor/widgets/<id>.widget.ts`: una clase `@Injectable()` que implementa `MonitorWidget` (`id`, `title`, `order`, `collect(): Promise<MonitorValue>`).
20
+ 2. `collect()` debe ser barato y de solo lectura: sin escrituras, sin SQL de negocio y sin depender de Redis. Si necesita la BD, usa una consulta técnica en un `*.sql.ts` y el repository de `common/`; si falla, deja que lance: el monitor lo muestra como "No disponible" y registra solo el tipo de error.
21
+ 3. Regístrala en `src/common/monitor/monitor.module.ts`, agregándola al arreglo `WIDGETS`. El multi-provider `MONITOR_WIDGETS` la recoge sola.
22
+ 4. `src/common/` no importa de `src/modules/`. Si el dato es de un módulo de negocio, no va al monitor base: regístralo como decisión.
23
+
24
+ ## 2. Tests
25
+
26
+ - `<id>.widget.spec.ts`: el valor y el desglose esperados, y que un error del origen se propaga (no se traga ni se formatea).
27
+ - Si cambia el HTML, ajusta `test/monitor.e2e-spec.ts` para que siga comprobando que no hay `<script>` y que un widget con error no rompe la página.
28
+
29
+ ## 3. Cierre
30
+
31
+ Agrega una entrada en `docs/EVOLUTIVO.md` (qué muestra ahora el monitor). Corre `./harness/init.sh` y revisa el checklist de `harness/playbooks/review.md` (un widget con datos sensibles es bloqueante).
@@ -0,0 +1,41 @@
1
+ # Playbook: revisar un cambio
2
+
3
+ Revisa el diff contra `AGENTS.md` y reporta cada hallazgo con archivo y línea, del más grave al menos grave.
4
+
5
+ ## Bloqueantes
6
+
7
+ - [ ] SQL fuera de `*.sql.ts`, o valores concatenados o interpolados en SQL
8
+ - [ ] `ORDER BY` o nombres de columna que no salen de una allowlist
9
+ - [ ] Cualquier escritura, DDL o migración
10
+ - [ ] Un endpoint nuevo sin guard JWT, o un `@Public()` fuera de `login`, `refresh` o `health`
11
+ - [ ] CORS abierto: `enableCors()` sin opciones, `origin: true`, `'*'`, reflejar el `Origin`, `credentials: true`, o `CORS_ORIGINS` con comodín
12
+ - [ ] El monitor sin `MonitorSessionGuard`, marcado `@Public()`, dentro de `/api/v1`, con `<script>`, o un widget que muestra secretos, SQL, errores crudos o datos de negocio
13
+ - [ ] `/auth/login` o `/monitor/login` sin `LoginAttemptLimiter`, un refresh que extiende la sesión (no copia `auth_time`), `src/main.ts` sin `securityHeaders()` o con `trust proxy` en `true`
14
+ - [ ] Tokens, contraseñas o secretos en logs, respuestas, código o commits
15
+ - [ ] `pino-pretty` en `dependencies` (va en devDependencies) o `LOG_FORMAT=pretty` para producción
16
+ - [ ] Cambios en `.env`, o `.env.local` incluido en el commit
17
+ - [ ] `process.env` fuera de `src/config/` en código nuevo (salvo tests), o un secreto con valor por defecto en `configuration.ts`
18
+ - [ ] Una dependencia nueva sin aprobación
19
+ - [ ] La API falla si Redis está caído o deshabilitado
20
+ - [ ] Se editó la base del harness (`AGENTS.md`, puentes o playbooks) en lugar de registrar la decisión en `harness/decisiones/`
21
+ - [ ] El cambio contradice una decisión `Aceptada` de `harness/decisiones/`, o toma una decisión nueva sin registrarla
22
+ - [ ] `./harness/init.sh` termina en 1 (crítico, `--strict` o `required`), o el cambio agrega advertencias nuevas respecto a la línea base
23
+ - [ ] Se silenció un chequeo en `harness/arness.config.json` sin aprobación humana o sin motivo
24
+
25
+ ## Contrato y arquitectura
26
+
27
+ - [ ] Estructura del endpoint según §5, y regla de agrupación (más de 2 archivos del mismo tipo en una carpeta)
28
+ - [ ] Fronteras: `common/` no importa de `modules/` y ningún módulo importa de otro módulo o cliente
29
+ - [ ] El service depende de la interfaz del repository, no de `pg`
30
+ - [ ] Envelope `{ data, meta? }` y errores `{ error: {...} }` con el status correcto
31
+ - [ ] Si cambió `.columns.ts` o los filtros, `FRONT.md` está actualizado y la versión de la clave de caché subió
32
+
33
+ ## Completitud
34
+
35
+ - [ ] Un spec por archivo, incluidos los casos HIT/MISS/BYPASS
36
+ - [ ] Postman, `FRONT.md` y el README del módulo actualizados
37
+ - [ ] Entrada nueva al final de `docs/EVOLUTIVO.md` (sin reescribir las anteriores), con fecha, hora y zona de Bogotá y el responsable (`git config user.name`) y lo abierto en `docs/PENDIENTES.md`
38
+ - [ ] `docs/EVOLUTIVO.md` no contiene pendientes, bloqueos, stoppers ni riesgos
39
+ - [ ] Variables nuevas en `.env.example`, `.env.local`, la validación y `src/config/configuration.ts` (`AppConfig`)
40
+ - [ ] `./harness/init.sh` termina en 0 sin advertencias nuevas; las preexistentes se reportan, no se corrigen fuera de alcance
41
+ - [ ] Código en inglés, docs en español, commit con formato Conventional Commits
@@ -0,0 +1,46 @@
1
+ # Playbook: actualizar Node a la última LTS
2
+
3
+ Referencia: `AGENTS.md` §2 (Node.js en su última LTS) y §13.4. Se usa cuando `./harness/init.sh` reporta `toolchain.node-lts` o cuando el humano pide actualizar Node.
4
+
5
+ **Actualizar Node es decisión humana.** El agente analiza, presenta y espera aprobación explícita. No cambia ningún archivo antes de esa aprobación, no instala Node por su cuenta y no convierte la advertencia en tarea si el humano no lo pide.
6
+
7
+ ## 1. Analizar (solo lectura)
8
+
9
+ 1. **Versiones:** la de `.nvmrc`, la de `node -v` y la última LTS:
10
+ ```bash
11
+ curl -s https://nodejs.org/dist/index.json \
12
+ | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const r=JSON.parse(s).find(x=>x.lts);console.log(r.version, r.lts)})'
13
+ ```
14
+ 2. **Soporte:** fin de soporte de la versión actual y de la nueva en `https://raw.githubusercontent.com/nodejs/Release/main/schedule.json` (campos `maintenance` y `end`).
15
+ 3. **Dónde está fijada la versión:** `.nvmrc`, `engines.node` de `package.json`, `FROM node:<major>-alpine` del `Dockerfile`, `azure-pipelines.yml` (`versionSpec` o lectura de `.nvmrc`) y `@types/node` en `devDependencies`. Lista cada archivo con su valor actual y el nuevo.
16
+ 4. **Compatibilidad:** `engines` de las dependencias directas en la versión instalada (`pnpm view <paquete>@<versión> engines`), empezando por `@nestjs/core`, `pg`, `ioredis` y `@types/node`. Marca cualquiera que no admita la nueva versión.
17
+ 5. **Cambios incompatibles de Node:** revisa las notas del release del nuevo major (`https://nodejs.org/en/blog`) y busca en `src/` y `test/` las APIs que se eliminan o cambian.
18
+ 6. **Entorno:** si `node -v` no es del nuevo major, el humano debe instalarlo (por ejemplo `nvm install --lts` o `fnm install --lts`) antes de verificar.
19
+
20
+ ## 2. Presentar al humano y pedir aprobación
21
+
22
+ ```
23
+ Actualización de Node: v<actual> → v<nueva LTS> (<codename>)
24
+ Soporte: v<actual> en mantenimiento hasta <fecha>, fin <fecha> · v<nueva> soportada hasta <fecha>
25
+ Archivos: .nvmrc, package.json (engines), Dockerfile, azure-pipelines.yml
26
+ Dependencias: @types/node <actual> → <nueva> (requiere aprobación, AGENTS.md §13.4)
27
+ Compatibilidad: <dependencias que admiten la nueva versión / las que no>
28
+ Riesgos: <APIs de Node afectadas en el código, o "ninguno detectado">
29
+ Recomendación: <actualizar ahora / esperar, y por qué>
30
+ ¿Apruebas la actualización? (sí / no)
31
+ ```
32
+
33
+ No continúes sin un **sí** explícito. La aprobación cubre solo lo listado: si aparece otra dependencia que cambiar, vuelve a preguntar.
34
+
35
+ ## 3. Si el humano aprueba
36
+
37
+ 1. Actualiza `.nvmrc` (versión completa, ej. `v26.11.0`), `engines.node` (`>=<major>`), la imagen del `Dockerfile` y `azure-pipelines.yml` si fija la versión.
38
+ 2. Actualiza `@types/node` al nuevo major con `pnpm add -D @types/node@<major>` y conserva el lockfile en el mismo cambio.
39
+ 3. Verifica con el nuevo Node: `pnpm install --frozen-lockfile`, `./harness/init.sh` (sin `toolchain.node` ni `toolchain.node-lts`), `pnpm test:e2e` y, si hay Docker, `docker build .`.
40
+ 4. Actualiza la versión de Node en `docs/TECNICO.md`.
41
+ 5. Commit: `chore(node): upgrade to Node <major> LTS`.
42
+
43
+ ## 4. Si el humano no aprueba
44
+
45
+ 1. Registra en `docs/PENDIENTES.md` una entrada de tipo recomendación con la fecha, la versión propuesta, el motivo que dio el humano y la fecha de fin de soporte de la versión actual.
46
+ 2. No silencies `toolchain.node-lts` en `harness/arness.config.json`: la advertencia sigue visible para recordarlo. Silenciarla es decisión humana y lleva motivo.
@@ -0,0 +1,111 @@
1
+ # Referencia: variables de entorno
2
+
3
+ Parte de la base del harness (AGENTS.md §6 a §10). Todas las variables:
4
+ - se declaran en `.env.example` con valores de ejemplo y en `.env.local` con valores locales;
5
+ - se validan en `src/config/` al arrancar.
6
+
7
+ `.env` nunca lo toca un agente. Si falta una variable requerida, la app falla al arrancar con un mensaje claro.
8
+
9
+ ## Dónde se leen las variables
10
+
11
+ `process.env` se lee **solo** en `src/config/configuration.ts` (plantilla `harness/skills/nest-base/templates/config/`). Ese archivo agrupa las variables por tema y convierte sus tipos; el resto del código recibe la configuración ya tipada.
12
+
13
+ ```typescript
14
+ // src/config/configuration.ts (extracto)
15
+ export function buildConfiguration(env: Env): AppConfig {
16
+ return {
17
+ app: { nodeEnv: text(env, 'NODE_ENV', 'development'), port: int(env, 'PORT', 3000) },
18
+ databases: parseDatabases(env), // DATABASES=MAIN,REPORTS → { MAIN: { host, port, ... }, REPORTS: {...} }
19
+ auth: parseAuthConfig(env), // src/config/auth.ts: secretos, límite de intentos y vida de la sesión
20
+ // cache, cors, monitor...
21
+ };
22
+ }
23
+ export default (): AppConfig => buildConfiguration(process.env);
24
+ ```
25
+
26
+ - **Registro:** `ConfigModule.forRoot({ isGlobal: true, envFilePath: ['.env.local', '.env'], validate, load: [configuration] })`. La validación (class-validator) revisa las variables crudas; `configuration.ts` solo agrupa y convierte.
27
+ - **Consumo:** `ConfigService<AppConfig, true>` con rutas tipadas, por ejemplo `config.get('cache.enabled', { infer: true })`. También en `main.ts` (`app.get(ConfigService)`): ni siquiera el puerto se lee de `process.env`.
28
+ - **Secretos sin valor por defecto** (`ACCESS_TOKEN`, `JWT_*_SECRET`, `DB_*_PASSWORD`): si faltan, la app no arranca. El error nombra la variable, nunca su valor. Los defaults son solo para valores no sensibles.
29
+ - **Tests:** los specs y `test/` pueden preparar `process.env`; `buildConfiguration(env)` es una función pura que se prueba con un objeto propio.
30
+ - **Verificación:** ESLint (`configBoundaries` de `eslint.boundaries.mjs`, regla `no-restricted-properties`) e `init.sh` (`env.process-env`, `env.configuration-file`, `env.eslint`).
31
+
32
+ **Agregar una variable** es, en el mismo cambio: `.env.example` + `.env.local` + validación + `configuration.ts` (y su tipo `AppConfig`) + esta referencia.
33
+
34
+ ## App
35
+
36
+ ```dotenv
37
+ NODE_ENV=development # development | test | production (default development)
38
+ PORT=3000
39
+ LOG_LEVEL=info # en desarrollo se recomienda debug
40
+ LOG_FORMAT=json # json (default, producción) | pretty (solo desarrollo local)
41
+ ```
42
+
43
+ - `LOG_FORMAT=pretty` requiere `pino-pretty` en devDependencies y no se acepta con `NODE_ENV=production`. `.env.local` usa `pretty`; `.env.example` documenta `json`.
44
+
45
+ ## Bases de datos (conexiones nombradas)
46
+
47
+ ```dotenv
48
+ DATABASES=MAIN,REPORTS # una sola BD: DATABASES=MAIN
49
+
50
+ DB_MAIN_HOST=localhost
51
+ DB_MAIN_PORT=5432
52
+ DB_MAIN_NAME=analytics
53
+ DB_MAIN_USER=readonly_user
54
+ DB_MAIN_PASSWORD=change-me
55
+ DB_MAIN_SSL=false
56
+ DB_MAIN_POOL_MAX=10
57
+ DB_MAIN_STATEMENT_TIMEOUT_MS=30000
58
+
59
+ DB_REPORTS_HOST=... # mismo bloque por cada nombre en DATABASES
60
+ ```
61
+
62
+ - `common/database` crea un `Pool` por nombre y lo expone con `getPoolToken('<NOMBRE>')`.
63
+ - Requeridas por conexión: `HOST`, `PORT`, `NAME`, `USER` y `PASSWORD`. `init.sh` las verifica en `.env.example`.
64
+
65
+ ## Caché
66
+
67
+ ```dotenv
68
+ CACHE_ENABLED=true
69
+ REDIS_URL=redis://localhost:6379
70
+ CACHE_DEFAULT_TTL_SECONDS=300
71
+ CACHE_KEY_PREFIX=arness
72
+ CACHE_OP_TIMEOUT_MS=200
73
+ ```
74
+
75
+ ## Autenticación
76
+
77
+ ```dotenv
78
+ ACCESS_TOKEN=example123 # contraseña compartida para /auth/login
79
+ JWT_ACCESS_SECRET=change-me
80
+ JWT_ACCESS_EXPIRES_IN=15m
81
+ JWT_REFRESH_SECRET=change-me-too # distinto de JWT_ACCESS_SECRET
82
+ JWT_REFRESH_EXPIRES_IN=7d
83
+ AUTH_SESSION_MAX_AGE_SECONDS=43200 # vida máxima de la sesión aunque se refresque (60–2592000; default 12 h)
84
+ AUTH_LOGIN_MAX_ATTEMPTS=5 # intentos fallidos por IP antes de bloquear el login (1–100)
85
+ AUTH_LOGIN_WINDOW_SECONDS=900 # ventana del conteo y duración del bloqueo (1–86400)
86
+ HTTP_TRUST_PROXY_HOPS=0 # proxies delante de la app (Azure App Service: 1); 0 = IP del socket
87
+ ```
88
+
89
+ - `src/config/auth.ts` (plantilla `templates/config/auth.ts`, `parseAuthConfig`) arma este bloque: los secretos son obligatorios y sin valor por defecto; el resto tiene default.
90
+ - Límite de intentos, vida de la sesión y proxy: `contrato-http.md` (Autenticación).
91
+
92
+ ## CORS
93
+
94
+ ```dotenv
95
+ CORS_ORIGINS=http://localhost:4200,https://reportes.example.com # obligatoria; orígenes exactos separados por comas
96
+ CORS_MAX_AGE_SECONDS=600 # opcional; caché del preflight (0–86400)
97
+ ```
98
+
99
+ - `CORS_ORIGINS` es **obligatoria**: sin ella la app no arranca. No admite `*` ni comodines, solo orígenes (`esquema://host[:puerto]`, sin ruta), y exige `https://` salvo `localhost`, `127.0.0.1` y `[::1]`.
100
+ - Cada entorno define su lista (desarrollo, QA, producción). `init.sh` verifica que `.env.example` la define y que no lleva `*`.
101
+
102
+ ## Monitor
103
+
104
+ ```dotenv
105
+ MONITOR_ENABLED=false # true para habilitar GET /monitor; con false responde 404
106
+ MONITOR_SESSION_EXPIRES_IN=15m # duración de la cookie de sesión del monitor
107
+ ```
108
+
109
+ - Por defecto `false`. Habilítalo solo donde el equipo lo necesite; en producción requiere aprobación.
110
+ - El login usa `ACCESS_TOKEN` y la sesión se firma con `JWT_ACCESS_SECRET`: no hay secretos nuevos.
111
+ - Si existe `src/common/monitor/`, `init.sh` verifica que `.env.example` define ambas variables.
@@ -0,0 +1,169 @@
1
+ # Referencia: contrato HTTP
2
+
3
+ Parte de la base del harness (AGENTS.md §8, §9 y §10).
4
+
5
+ ## Respuesta exitosa
6
+
7
+ `meta` aparece **solo** cuando el endpoint pagina o filtra.
8
+
9
+ ```jsonc
10
+ // Sin paginación ni filtros
11
+ { "data": [ /* filas */ ] }
12
+
13
+ // Con paginación y/o filtros
14
+ {
15
+ "data": [ /* filas */ ],
16
+ "meta": {
17
+ "pagination": { "page": 1, "pageSize": 50, "total": 1234, "totalPages": 25 },
18
+ "filters": { "desde": "2026-01-01", "regional": "ANT" }
19
+ }
20
+ }
21
+ ```
22
+
23
+ ## Respuesta de error
24
+
25
+ Siempre con el status HTTP correcto. `details` es opcional.
26
+
27
+ ```json
28
+ {
29
+ "error": {
30
+ "code": "VALIDATION_ERROR",
31
+ "message": "El parámetro 'desde' debe ser una fecha ISO",
32
+ "details": [{ "field": "desde", "issue": "isDateString" }],
33
+ "requestId": "b1f2c3...",
34
+ "path": "/api/v1/cartera/resumen",
35
+ "timestamp": "2026-09-25T14:00:00.000Z"
36
+ }
37
+ }
38
+ ```
39
+
40
+ | Status | `code` | Origen típico |
41
+ |---|---|---|
42
+ | 400 | `VALIDATION_ERROR` | DTO inválido |
43
+ | 401 | `UNAUTHORIZED` / `TOKEN_EXPIRED` / `INVALID_CREDENTIALS` / `SESSION_EXPIRED` | guard, login o refresh |
44
+ | 403 | `FORBIDDEN` | — |
45
+ | 404 | `NOT_FOUND` | ruta o recurso |
46
+ | 429 | `TOO_MANY_ATTEMPTS` | login bloqueado por intentos fallidos (con `Retry-After`) |
47
+ | 503 | `DATABASE_UNAVAILABLE` | pg: conexión rechazada, `08*`, `57P01` |
48
+ | 504 | `QUERY_TIMEOUT` | pg: `57014` (statement_timeout) |
49
+ | 500 | `INTERNAL_ERROR` | cualquier otro; sin stack ni SQL |
50
+
51
+ ## Autenticación
52
+
53
+ | Endpoint | Body | Respuesta |
54
+ |---|---|---|
55
+ | `POST /api/v1/auth/login` | `{ "password": "..." }` | `{ data: { accessToken, refreshToken, expiresIn } }` |
56
+ | `POST /api/v1/auth/refresh` | `{ "refreshToken": "..." }` | `{ data: { accessToken, refreshToken, expiresIn } }` |
57
+
58
+ - `password` se compara con `ACCESS_TOKEN` en tiempo constante: `crypto.timingSafeEqual` sobre el sha256 de ambos valores.
59
+ - Los tokens usan secretos distintos; el claim `type` (`access` o `refresh`) se valida. Un token de otro tipo (`refresh`, o el `monitor` de la cookie) como `Bearer` → `401 UNAUTHORIZED`; un access token en `/auth/refresh` → `401 UNAUTHORIZED`.
60
+ - Los clientes envían `Authorization: Bearer <accessToken>`. Ante `401 TOKEN_EXPIRED`, llaman a `/auth/refresh` y reintentan una vez. Ante `401 SESSION_EXPIRED` o `429`, vuelven al login.
61
+
62
+ ### Vida máxima de la sesión
63
+
64
+ - `/auth/login` emite el par con el claim `auth_time` (segundos, hora del login). `/auth/refresh` **copia** el `auth_time` del refresh token al par nuevo: refrescar nunca extiende la sesión.
65
+ - Si `ahora - auth_time >= AUTH_SESSION_MAX_AGE_SECONDS` (default 12 h), o el refresh token no trae `auth_time`, el refresh responde `401 SESSION_EXPIRED` y el cliente debe volver a `/auth/login`. Así, un refresh token robado deja de servir como mucho al cumplir esa edad.
66
+ - Para cortar todas las sesiones de inmediato, se rota `JWT_REFRESH_SECRET` (y `JWT_ACCESS_SECRET` si hace falta).
67
+ - Implementación de referencia: `templates/security/session-lifetime.ts` → `src/common/security/` (`sessionClaims`, `isSessionExpired`).
68
+
69
+ ### Límite de intentos
70
+
71
+ - `/auth/login` y `/monitor/login` comparten `LoginAttemptLimiter` (`templates/security/login-attempts.ts` → `src/common/security/`, provisto por `SecurityModule`, global). Cuenta los intentos fallidos **por IP** (`req.ip`) en memoria.
72
+ - Al llegar a `AUTH_LOGIN_MAX_ATTEMPTS` fallos (default 5) dentro de `AUTH_LOGIN_WINDOW_SECONDS` (default 900, contados desde el primer fallo), la IP queda bloqueada hasta que termina la ventana: responde `429 TOO_MANY_ATTEMPTS` con `Retry-After: <segundos>` **sin comparar la contraseña**. Un login correcto reinicia el contador.
73
+ - Orden en el controller: `check(ip)` → si no está permitido, `429`; si la contraseña es incorrecta, `registerFailure(ip)` y `401 INVALID_CREDENTIALS`; si es correcta, `reset(ip)` y el par de tokens.
74
+ - **IP real detrás de un proxy:** con `HTTP_TRUST_PROXY_HOPS=<n>` (Azure App Service o un gateway: `1`), `main.ts` llama a `app.set('trust proxy', n)` y `req.ip` sale de `X-Forwarded-For`. Con `0` (default) se usa la IP del socket. Nunca `true`: permitiría falsificar la IP con la cabecera.
75
+ - El contador es **por instancia** y no depende de Redis: con varias réplicas, cada una cuenta aparte. El map tiene un tope de 10 000 IPs.
76
+
77
+ ## Cabeceras de seguridad
78
+
79
+ `securityHeaders()` (`templates/security/security-headers.ts` → `src/common/security/`) se registra en `src/main.ts` con `app.use(...)` antes de las rutas, sin helmet. En toda respuesta:
80
+
81
+ | Cabecera | Valor |
82
+ |---|---|
83
+ | `Content-Security-Policy` | `default-src 'none'; frame-ancestors 'none'; base-uri 'none'` |
84
+ | `X-Content-Type-Options` | `nosniff` |
85
+ | `X-Frame-Options` | `DENY` |
86
+ | `Referrer-Policy` | `no-referrer` |
87
+ | `Strict-Transport-Security` | `max-age=31536000` |
88
+ | `Cache-Control` | `no-store` (las respuestas llevan datos privados; la caché es la de Redis) |
89
+
90
+ Además quita `X-Powered-By`. El monitor reemplaza la CSP por la suya (sección Monitor).
91
+
92
+ ## CORS
93
+
94
+ Allowlist por entorno: solo los orígenes de `CORS_ORIGINS` reciben `Access-Control-Allow-Origin`. Implementación de referencia: `harness/skills/nest-base/templates/cors/cors.ts` → `src/config/cors.ts`.
95
+
96
+ | Opción de `app.enableCors()` | Valor |
97
+ |---|---|
98
+ | `origin` | función que acepta solo los orígenes exactos de `CORS_ORIGINS` (sin `Origin` → sin cabeceras CORS) |
99
+ | `methods` | `GET, POST, OPTIONS` |
100
+ | `allowedHeaders` | `Authorization, Content-Type, X-Request-Id` |
101
+ | `exposedHeaders` | `X-Cache, X-Request-Id` |
102
+ | `credentials` | `false` (la API usa `Bearer`, no cookies) |
103
+ | `maxAge` | `CORS_MAX_AGE_SECONDS` (default 600) |
104
+ | `optionsSuccessStatus` | `204` |
105
+
106
+ - **Validación al arrancar** (`src/config/`): `CORS_ORIGINS` es obligatoria; se rechazan `*` y comodines, valores que no son un origen (`esquema://host[:puerto]`, sin ruta) y `http://` fuera de `localhost`, `127.0.0.1` o `[::1]`.
107
+ - **Prohibido:** `enableCors()` sin opciones, `origin: true`, `origin: '*'`, reflejar el `Origin` recibido o `credentials: true`.
108
+ - **Tests:** `src/config/cors.spec.ts` (parseo y validación) y `test/cors.e2e-spec.ts`: preflight permitido → `204` con `Access-Control-Allow-Origin`, métodos, cabeceras y `max-age`; preflight de un origen no permitido → sin `Access-Control-Allow-Origin`.
109
+ - `init.sh` advierte si `.env.example` no define `CORS_ORIGINS` o lo define con `*`, y si `src/main.ts` no llama a `enableCors` o lo abre a cualquier origen.
110
+
111
+ ## Caché
112
+
113
+ - Header `X-Cache: HIT | MISS | BYPASS` en toda respuesta cacheable.
114
+ - Clave: `<prefix>:<cliente>:<modulo>:<endpoint>:v<version>:<hash-estable-de-filtros>`. El hash se calcula con sha256 sobre el JSON de los filtros con las claves ordenadas.
115
+
116
+ ## Logging
117
+
118
+ - JSON estructurado (`nestjs-pino`). `requestId` desde `X-Request-Id` (si es seguro: `[A-Za-z0-9._-]`, máximo 128) o UUID generado, devuelto en la respuesta.
119
+ - Por request: método, ruta, status, duración, resultado de caché y conexión de BD.
120
+ - Redact: `authorization`, cookies (`cookie` y `set-cookie`), `password`, `accessToken`, `refreshToken`, `*_SECRET` y `*_PASSWORD`.
121
+ - Plantilla: `harness/skills/nest-base/templates/logging/logger-options.ts` (`buildLoggerParams`, `parseLogFormat`, `REDACT_PATHS`).
122
+
123
+ ### Formato por entorno (`LOG_FORMAT`)
124
+
125
+ | Entorno | `LOG_FORMAT` | Ejemplo |
126
+ |---|---|---|
127
+ | Producción, QA, CI (y por defecto) | `json` | `{"level":30,"requestId":"a1b2c3","req":{"method":"GET","url":"/api/v1/cartera/resumen"},"res":{"statusCode":200},"responseTime":42,"msg":"GET /api/v1/cartera/resumen 200 42ms"}` |
128
+ | Desarrollo local | `pretty` | `10:21:03 INFO: [a1b2c3] GET /api/v1/cartera/resumen 200 42ms {"cache":"HIT"}` |
129
+
130
+ - Ambos formatos llevan los mismos campos y la **misma redacción**: el formato legible nunca muestra secretos.
131
+ - `pretty` usa `pino-pretty`, que es **devDependency**: la imagen de producción no lo incluye. Si se pide `pretty` y no está instalado, el logger vuelve a JSON.
132
+ - La validación rechaza `LOG_FORMAT=pretty` con `NODE_ENV=production`, y cualquier valor distinto de `json` o `pretty`.
133
+ - Los logs no incluyen el SQL ejecutado ni sus parámetros en ningún formato.
134
+
135
+ ## Health
136
+
137
+ `GET /health` es público y usa terminus. Indicadores:
138
+ - `db_<nombre en minúsculas>`, uno por conexión;
139
+ - `cache`, con estado `up`, `down` o `disabled`.
140
+
141
+ | Situación | Respuesta |
142
+ |---|---|
143
+ | Todas las BDs arriba | `200` |
144
+ | Alguna BD caída | `503` |
145
+ | Redis caído o deshabilitado | sigue en `200`; `cache: down / disabled` |
146
+
147
+ `init.sh` consume este formato para informar qué base está caída.
148
+
149
+ ## Monitor
150
+
151
+ Tablero de estado para el equipo, **fuera de `/api/v1`** y apagado por defecto. Implementación de referencia: `harness/skills/nest-base/templates/monitor/` → `src/common/monitor/`.
152
+
153
+ | Ruta | Sesión | Respuesta |
154
+ |---|---|---|
155
+ | `GET /monitor` | cookie | HTML con los widgets; sin sesión → `303` a `/monitor/login` |
156
+ | `GET /monitor/login` | — | formulario HTML (`password`) |
157
+ | `POST /monitor/login` | — | correcta → `303` a `/monitor` y `Set-Cookie`; incorrecta → `401` con el formulario y sin cookie |
158
+ | `POST /monitor/logout` | — | borra la cookie (`Max-Age=0`) y `303` a `/monitor/login` |
159
+
160
+ - **Activación:** `MONITOR_ENABLED=true`. Con cualquier otro valor, todas las rutas responden `404`.
161
+ - **Render:** `react-dom/server` (`renderToStaticMarkup`) con `createElement`, sin JSX, sin hidratación, sin bundler y sin `<script>`. Estilos inline.
162
+ - **Login:** la contraseña se compara con `ACCESS_TOKEN` en tiempo constante, igual que `/auth/login`. Al acertar se emite un JWT con `type: "monitor"` (firmado con `JWT_ACCESS_SECRET`, dura `MONITOR_SESSION_EXPIRES_IN`) en la cookie `arness_monitor`: `HttpOnly; Secure; SameSite=Strict; Path=/monitor; Max-Age=<duración>`. Un access token (`type: "access"`) no sirve como cookie ni la cookie sirve como `Bearer`.
163
+ - **Guards:** el controller lleva `@MonitorRoute()`, que `JwtAuthGuard` omite, y `MonitorSessionGuard`, que exige la cookie salvo en los handlers `@MonitorLogin()` (formulario, login y logout). No es `@Public()`.
164
+ - **Límite de intentos:** comparte `LoginAttemptLimiter` con `/auth/login` (sección Autenticación). Bloqueado → `429` con `Retry-After` y el formulario con "Demasiados intentos", sin cookie.
165
+ - **Cabeceras en toda respuesta:** `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'`, `Cache-Control: no-store`, `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff` y `Referrer-Policy: no-referrer`.
166
+ - **Widgets:** `MonitorWidget { id; title; order; collect(): Promise<MonitorValue> }`, registrados en el multi-provider `MONITOR_WIDGETS` de `MonitorModule`. Un widget que lanza o tarda más de 2 s se muestra como "No disponible"; el log registra solo el tipo de error, nunca su mensaje.
167
+ - **Widget base `endpoints-total`:** cuenta las rutas HTTP con `DiscoveryService` y `MetadataScanner`, excluye `monitor`, `health` y `auth`, y desglosa por el primer segmento de la ruta (el módulo).
168
+ - **Nunca se muestran:** secretos, variables de entorno, SQL, errores de PostgreSQL ni datos de negocio (filas, montos, nombres de clientes). Solo conteos y estados técnicos.
169
+ - **Tests:** unit de servicio, página, sesión, guard y widget; e2e `test/monitor.e2e-spec.ts` (redirección sin cookie, `401` sin cookie con contraseña incorrecta, login y conteo correcto con controllers de prueba, widget con error que no rompe la página, sin `<script>`, `404` con `MONITOR_ENABLED=false`).