@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.
- package/CHANGELOG.md +85 -0
- package/README.md +156 -2
- package/bin/arness.mjs +710 -0
- package/harness/AGENTS.md +238 -0
- package/harness/agents/implementador.md +42 -0
- package/harness/agents/lider.md +72 -0
- package/harness/agents/revisor.md +62 -0
- package/harness/decisiones/README.md +17 -0
- package/harness/decisiones/_plantilla.md +24 -0
- package/harness/init.sh +743 -0
- package/harness/playbooks/add-database.md +16 -0
- package/harness/playbooks/new-endpoint.md +88 -0
- package/harness/playbooks/new-module.md +21 -0
- package/harness/playbooks/new-monitor-widget.md +31 -0
- package/harness/playbooks/review.md +41 -0
- package/harness/playbooks/update-node-lts.md +46 -0
- package/harness/reference/configuracion.md +111 -0
- package/harness/reference/contrato-http.md +169 -0
- package/harness/reference/novedades.json +158 -0
- package/harness/reference/progress-plantilla.md +50 -0
- package/harness/skills/commit-hygiene/SKILL.md +137 -0
- package/harness/skills/nest-base/SKILL.md +168 -0
- package/harness/skills/nest-base/templates/config/auth.spec.ts +32 -0
- package/harness/skills/nest-base/templates/config/auth.ts +80 -0
- package/harness/skills/nest-base/templates/config/configuration.spec.ts +85 -0
- package/harness/skills/nest-base/templates/config/configuration.ts +137 -0
- package/harness/skills/nest-base/templates/cors/cors.spec.ts +53 -0
- package/harness/skills/nest-base/templates/cors/cors.ts +75 -0
- package/harness/skills/nest-base/templates/docs/EVOLUTIVO.md +27 -0
- package/harness/skills/nest-base/templates/docs/PENDIENTES.md +20 -0
- package/harness/skills/nest-base/templates/docs/TECNICO.md +43 -0
- package/harness/skills/nest-base/templates/e2e/cors.e2e-spec.ts +60 -0
- package/harness/skills/nest-base/templates/e2e/monitor.e2e-spec.ts +151 -0
- package/harness/skills/nest-base/templates/editorconfig +12 -0
- package/harness/skills/nest-base/templates/eslint.boundaries.mjs +65 -0
- package/harness/skills/nest-base/templates/eslint.config.mjs +82 -0
- package/harness/skills/nest-base/templates/logging/logger-options.spec.ts +57 -0
- package/harness/skills/nest-base/templates/logging/logger-options.ts +85 -0
- package/harness/skills/nest-base/templates/monitor/monitor-login-redirect.filter.ts +16 -0
- package/harness/skills/nest-base/templates/monitor/monitor-login.dto.ts +8 -0
- package/harness/skills/nest-base/templates/monitor/monitor-page.spec.ts +41 -0
- package/harness/skills/nest-base/templates/monitor/monitor-page.ts +190 -0
- package/harness/skills/nest-base/templates/monitor/monitor-session.guard.spec.ts +46 -0
- package/harness/skills/nest-base/templates/monitor/monitor-session.guard.ts +36 -0
- package/harness/skills/nest-base/templates/monitor/monitor-session.service.spec.ts +57 -0
- package/harness/skills/nest-base/templates/monitor/monitor-session.service.ts +77 -0
- package/harness/skills/nest-base/templates/monitor/monitor-widget.interface.ts +18 -0
- package/harness/skills/nest-base/templates/monitor/monitor.constants.ts +25 -0
- package/harness/skills/nest-base/templates/monitor/monitor.controller.ts +84 -0
- package/harness/skills/nest-base/templates/monitor/monitor.decorators.ts +11 -0
- package/harness/skills/nest-base/templates/monitor/monitor.module.ts +30 -0
- package/harness/skills/nest-base/templates/monitor/monitor.service.spec.ts +55 -0
- package/harness/skills/nest-base/templates/monitor/monitor.service.ts +49 -0
- package/harness/skills/nest-base/templates/monitor/widgets/endpoints-total.widget.spec.ts +51 -0
- package/harness/skills/nest-base/templates/monitor/widgets/endpoints-total.widget.ts +53 -0
- package/harness/skills/nest-base/templates/prettierignore +6 -0
- package/harness/skills/nest-base/templates/prettierrc.json +8 -0
- package/harness/skills/nest-base/templates/security/login-attempts.spec.ts +48 -0
- package/harness/skills/nest-base/templates/security/login-attempts.ts +90 -0
- package/harness/skills/nest-base/templates/security/security-headers.spec.ts +27 -0
- package/harness/skills/nest-base/templates/security/security-headers.ts +21 -0
- package/harness/skills/nest-base/templates/security/security.module.ts +20 -0
- package/harness/skills/nest-base/templates/security/session-lifetime.spec.ts +21 -0
- package/harness/skills/nest-base/templates/security/session-lifetime.ts +28 -0
- 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`).
|