@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,158 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"description": "Novedades adoptables por versión del harness. Las lee `arness doctor` (qué falta adoptar) y `arness sync` (qué trae la versión). Tipos: seguridad | recomendada | opcional. Una novedad está adoptada cuando existen sus `paths` y init.sh no reporta ninguno de sus `checks`.",
|
|
4
|
+
"novedades": [
|
|
5
|
+
{
|
|
6
|
+
"id": "cors-allowlist",
|
|
7
|
+
"since": "0.2.0",
|
|
8
|
+
"type": "seguridad",
|
|
9
|
+
"title": "CORS con allowlist por entorno",
|
|
10
|
+
"why": "Sin allowlist, cualquier sitio web puede llamar la API desde el navegador de un usuario autenticado.",
|
|
11
|
+
"checks": ["security.cors-env", "security.cors", "security.cors-open"],
|
|
12
|
+
"paths": [],
|
|
13
|
+
"approval": null,
|
|
14
|
+
"steps": [
|
|
15
|
+
"Copia harness/skills/nest-base/templates/cors/ (cors.ts y su spec) a src/config/ y valida CORS_ORIGINS al arrancar.",
|
|
16
|
+
"En src/main.ts usa app.enableCors(buildCorsOptions(...)) con los valores validados.",
|
|
17
|
+
"Agrega CORS_ORIGINS y CORS_MAX_AGE_SECONDS a .env.example y .env.local, y define los orígenes de cada entorno antes de desplegar."
|
|
18
|
+
],
|
|
19
|
+
"adr": "0006",
|
|
20
|
+
"reference": "harness/reference/contrato-http.md (CORS)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "eslint-boundaries",
|
|
24
|
+
"since": "0.2.0",
|
|
25
|
+
"type": "recomendada",
|
|
26
|
+
"title": "Fronteras de arquitectura verificadas por ESLint",
|
|
27
|
+
"why": "Evita que common/ dependa de los módulos y que un módulo o cliente importe de otro.",
|
|
28
|
+
"checks": ["architecture.eslint-boundaries"],
|
|
29
|
+
"paths": ["eslint.boundaries.mjs"],
|
|
30
|
+
"approval": null,
|
|
31
|
+
"steps": [
|
|
32
|
+
"Copia harness/skills/nest-base/templates/eslint.boundaries.mjs a la raíz.",
|
|
33
|
+
"Impórtala en eslint.config.mjs y agrega ...architectureBoundaries.",
|
|
34
|
+
"Si aparecen imports entre módulos, mueve lo compartido a src/common/ cuando toques ese módulo."
|
|
35
|
+
],
|
|
36
|
+
"adr": "0007",
|
|
37
|
+
"reference": "harness/AGENTS.md §4"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "monitor",
|
|
41
|
+
"since": "0.2.0",
|
|
42
|
+
"type": "opcional",
|
|
43
|
+
"title": "Tablero monitor (GET /monitor)",
|
|
44
|
+
"why": "Muestra el estado técnico del backend (endpoints, salud) sin exponer datos de negocio.",
|
|
45
|
+
"checks": ["security.monitor-env", "security.monitor-guard"],
|
|
46
|
+
"paths": ["src/common/monitor"],
|
|
47
|
+
"approval": "react y react-dom (aprobadas en AGENTS.md §2 solo para el monitor); registra la adopción en harness/decisiones/ si el proyecto aún no las usa.",
|
|
48
|
+
"steps": [
|
|
49
|
+
"Copia harness/skills/nest-base/templates/monitor/ a src/common/monitor/ y registra MonitorModule. Requiere SecurityModule (templates/security/), que comparte el límite de intentos del login.",
|
|
50
|
+
"Excluye monitor del prefijo global y haz que JwtAuthGuard omita IS_MONITOR_ROUTE_KEY.",
|
|
51
|
+
"Agrega MONITOR_ENABLED=false y MONITOR_SESSION_EXPIRES_IN=15m a .env.example y .env.local."
|
|
52
|
+
],
|
|
53
|
+
"adr": "0008",
|
|
54
|
+
"reference": "harness/playbooks/new-monitor-widget.md"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "env-config",
|
|
58
|
+
"since": "0.2.0",
|
|
59
|
+
"type": "recomendada",
|
|
60
|
+
"title": "Configuración centralizada en src/config/configuration.ts",
|
|
61
|
+
"why": "Toda la configuración tipada en un solo lugar, sin process.env disperso ni secretos con valor por defecto.",
|
|
62
|
+
"checks": ["env.configuration-file", "env.process-env", "env.eslint"],
|
|
63
|
+
"paths": [],
|
|
64
|
+
"approval": null,
|
|
65
|
+
"steps": [
|
|
66
|
+
"Copia harness/skills/nest-base/templates/config/ (configuration.ts y su spec) a src/config/ y regístrala con ConfigModule.forRoot({ load: [configuration] }).",
|
|
67
|
+
"Cambia, módulo por módulo, cada process.env.X por ConfigService<AppConfig, true>.",
|
|
68
|
+
"Actualiza eslint.boundaries.mjs desde la plantilla y agrega ...configBoundaries en eslint.config.mjs."
|
|
69
|
+
],
|
|
70
|
+
"adr": "0009",
|
|
71
|
+
"reference": "harness/reference/configuracion.md (Dónde se leen las variables)"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "log-format",
|
|
75
|
+
"since": "0.2.0",
|
|
76
|
+
"type": "opcional",
|
|
77
|
+
"title": "Logs legibles en desarrollo (LOG_FORMAT=pretty)",
|
|
78
|
+
"why": "Una línea legible por petición en la terminal local; producción sigue en JSON.",
|
|
79
|
+
"checks": ["logging.format-env"],
|
|
80
|
+
"paths": [],
|
|
81
|
+
"approval": "pino-pretty, solo como devDependency (aprobada en AGENTS.md §2).",
|
|
82
|
+
"steps": [
|
|
83
|
+
"pnpm add -D pino-pretty",
|
|
84
|
+
"Copia harness/skills/nest-base/templates/logging/ a src/common/logging/ y usa buildLoggerParams en LoggerModule.forRootAsync.",
|
|
85
|
+
"Agrega LOG_FORMAT=json a .env.example y LOG_FORMAT=pretty a .env.local."
|
|
86
|
+
],
|
|
87
|
+
"adr": "0010",
|
|
88
|
+
"reference": "harness/reference/contrato-http.md (Logging)"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "project-docs",
|
|
92
|
+
"since": "0.2.0",
|
|
93
|
+
"type": "recomendada",
|
|
94
|
+
"title": "Documentación del proyecto en docs/ (técnico, evolutivo y pendientes)",
|
|
95
|
+
"why": "Negocio ve lo entregado en EVOLUTIVO.md y lo abierto queda en PENDIENTES.md, sin mezclarse.",
|
|
96
|
+
"checks": ["docs.files"],
|
|
97
|
+
"paths": [],
|
|
98
|
+
"approval": null,
|
|
99
|
+
"steps": [
|
|
100
|
+
"Copia harness/skills/nest-base/templates/docs/ a docs/.",
|
|
101
|
+
"Completa docs/TECNICO.md con lo que ya existe y, si quieres, resume lo entregado en docs/EVOLUTIVO.md."
|
|
102
|
+
],
|
|
103
|
+
"adr": "0011",
|
|
104
|
+
"reference": "harness/AGENTS.md §12"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"id": "login-rate-limit",
|
|
108
|
+
"since": "0.3.0",
|
|
109
|
+
"type": "seguridad",
|
|
110
|
+
"title": "Límite de intentos en /auth/login y /monitor/login",
|
|
111
|
+
"why": "Toda la API se protege con una contraseña compartida: sin límite se puede adivinar por fuerza bruta.",
|
|
112
|
+
"checks": ["security.login-rate-limit", "security.trust-proxy"],
|
|
113
|
+
"paths": [],
|
|
114
|
+
"approval": null,
|
|
115
|
+
"steps": [
|
|
116
|
+
"Copia harness/skills/nest-base/templates/security/ a src/common/security/ y registra SecurityModule en app.module.ts. Copia también templates/config/auth.ts (y su spec) y usa auth: parseAuthConfig(env) en configuration.ts.",
|
|
117
|
+
"En el login: check(req.ip) → 429 TOO_MANY_ATTEMPTS con Retry-After; contraseña incorrecta → registerFailure(req.ip); correcta → reset(req.ip). Copia también templates/monitor/monitor.controller.ts si usas el monitor.",
|
|
118
|
+
"Agrega AUTH_LOGIN_MAX_ATTEMPTS, AUTH_LOGIN_WINDOW_SECONDS y HTTP_TRUST_PROXY_HOPS (1 detrás de Azure App Service) y, en main.ts, app.set(\"trust proxy\", hops) solo si hops > 0.",
|
|
119
|
+
"Avisa a frontend: ante 429 se espera lo que indica Retry-After (FRONT.md)."
|
|
120
|
+
],
|
|
121
|
+
"adr": "0017",
|
|
122
|
+
"reference": "harness/reference/contrato-http.md (Autenticación › Límite de intentos)"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"id": "session-max-age",
|
|
126
|
+
"since": "0.3.0",
|
|
127
|
+
"type": "seguridad",
|
|
128
|
+
"title": "Vida máxima de la sesión en /auth/refresh",
|
|
129
|
+
"why": "Cada refresh entregaba un par nuevo sin límite: un refresh token robado servía indefinidamente.",
|
|
130
|
+
"checks": ["security.session-max-age"],
|
|
131
|
+
"paths": [],
|
|
132
|
+
"approval": null,
|
|
133
|
+
"steps": [
|
|
134
|
+
"Con src/common/security/ ya copiado, firma el par con sessionClaims(authTime): el login usa nowInSeconds() y el refresh copia el auth_time del refresh token.",
|
|
135
|
+
"En el refresh, si isSessionExpired(payload, sessionMaxAgeSeconds) responde 401 SESSION_EXPIRED.",
|
|
136
|
+
"Agrega AUTH_SESSION_MAX_AGE_SECONDS=43200 a .env.example y .env.local, y avisa a frontend: ante 401 SESSION_EXPIRED vuelve al login. Los refresh tokens emitidos antes no traen auth_time y exigirán un login nuevo."
|
|
137
|
+
],
|
|
138
|
+
"adr": "0017",
|
|
139
|
+
"reference": "harness/reference/contrato-http.md (Autenticación › Vida máxima de la sesión)"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"id": "security-headers",
|
|
143
|
+
"since": "0.3.0",
|
|
144
|
+
"type": "seguridad",
|
|
145
|
+
"title": "Cabeceras de seguridad en toda respuesta",
|
|
146
|
+
"why": "Sin CSP, nosniff, X-Frame-Options ni no-store, el navegador puede interpretar o guardar en caché respuestas con datos privados.",
|
|
147
|
+
"checks": ["security.headers"],
|
|
148
|
+
"paths": [],
|
|
149
|
+
"approval": null,
|
|
150
|
+
"steps": [
|
|
151
|
+
"Con src/common/security/ ya copiado, agrega app.use(securityHeaders()) en src/main.ts antes de registrar las rutas.",
|
|
152
|
+
"Verifica en test/ que una respuesta de la API trae las cabeceras y no trae X-Powered-By."
|
|
153
|
+
],
|
|
154
|
+
"adr": "0017",
|
|
155
|
+
"reference": "harness/reference/contrato-http.md (Cabeceras de seguridad)"
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Progress
|
|
2
|
+
|
|
3
|
+
> Estado compartido del trabajo en curso. **Solo lo escribe el `lider`**; el `implementador` y el `revisor` lo leen.
|
|
4
|
+
> Aquí van resúmenes, no diffs ni logs completos. Mantenlo por debajo de ~150 líneas: las tareas cerradas se reducen a una línea en la tabla.
|
|
5
|
+
|
|
6
|
+
## Objetivo
|
|
7
|
+
|
|
8
|
+
<Qué pidió el humano, en una o dos frases.>
|
|
9
|
+
|
|
10
|
+
## Estado
|
|
11
|
+
|
|
12
|
+
- **Estado:** EN CURSO
|
|
13
|
+
- **Actualizado:** AAAA-MM-DD HH:MM
|
|
14
|
+
- **init.sh (última ejecución):** <fecha> · <exit 0 + N advertencias | exit 1: resumen de fallos>
|
|
15
|
+
- **Línea base de advertencias (inicio de la sesión):** <ids de init.sh --json, o "ninguna">
|
|
16
|
+
|
|
17
|
+
## Contrato confirmado
|
|
18
|
+
|
|
19
|
+
<Lo confirmado con el humano (AGENTS.md §15.1): cliente/módulo/segmento, conexión y origen, columnas, filtros, paginación, TTL. Escribe "—" si no aplica.>
|
|
20
|
+
|
|
21
|
+
## Decisiones de esta sesión
|
|
22
|
+
|
|
23
|
+
- <Enlaces a harness/decisiones/NNNN-*.md aceptadas o propuestas, y acuerdos del humano que aún no son decisión formal.>
|
|
24
|
+
|
|
25
|
+
## Tareas
|
|
26
|
+
|
|
27
|
+
| ID | Tarea | Responsable | Estado | Ronda | Veredicto |
|
|
28
|
+
|---|---|---|---|---|---|
|
|
29
|
+
| T-001 | <título> | implementador | PENDIENTE | 0 | — |
|
|
30
|
+
|
|
31
|
+
Estados: `PENDIENTE` · `EN CURSO` · `EN REVISIÓN` · `CAMBIOS REQUERIDOS` · `APROBADA` · `BLOQUEADA`.
|
|
32
|
+
|
|
33
|
+
## Detalle por tarea
|
|
34
|
+
|
|
35
|
+
### T-001 · <título>
|
|
36
|
+
|
|
37
|
+
- **Encargo:** objetivo, playbook, archivos y criterios de aceptación.
|
|
38
|
+
- **Fuera de alcance:** <...>
|
|
39
|
+
- **Última entrega:** <resumen del reporte del implementador>
|
|
40
|
+
- **Último veredicto:** <resumen de los hallazgos del revisor, con archivo:línea>
|
|
41
|
+
|
|
42
|
+
## Bloqueos y preguntas abiertas
|
|
43
|
+
|
|
44
|
+
- <Pregunta o bloqueo · a quién le toca responder>
|
|
45
|
+
|
|
46
|
+
## Bitácora
|
|
47
|
+
|
|
48
|
+
<Append-only: una línea por evento.>
|
|
49
|
+
|
|
50
|
+
- AAAA-MM-DD HH:MM · lider · <evento>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: engram-commit-hygiene
|
|
3
|
+
description: >
|
|
4
|
+
Commit and branch naming standards for Engram contributors, enforced by GitHub rulesets.
|
|
5
|
+
Trigger: Any commit creation, review, or branch cleanup.
|
|
6
|
+
license: Apache-2.0
|
|
7
|
+
metadata:
|
|
8
|
+
author: gentleman-programming
|
|
9
|
+
version: "2.0"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
Use this skill when:
|
|
15
|
+
- Creating commits
|
|
16
|
+
- Creating or naming branches
|
|
17
|
+
- Reviewing commit history in a PR
|
|
18
|
+
- Cleaning up staged changes
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Critical Rules
|
|
23
|
+
|
|
24
|
+
1. **Commit messages MUST follow Conventional Commits** — enforced by GitHub ruleset, rejected on push if invalid
|
|
25
|
+
2. **Branch names MUST follow `type/description` format** — enforced by GitHub ruleset, rejected on push if invalid
|
|
26
|
+
3. Keep one logical change per commit
|
|
27
|
+
4. Message should explain **why**, not only what
|
|
28
|
+
5. **NEVER** include `Co-Authored-By` trailers
|
|
29
|
+
6. **NEVER** commit paths prohibited by the [Transient Artifact Policy](../../CONTRIBUTING.md#transient-artifact-policy)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Commit Message Format (enforced by GitHub ruleset)
|
|
34
|
+
|
|
35
|
+
**Regex:** `^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9\._-]+\))?!?: .+`
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
<type>(<optional-scope>): <description>
|
|
39
|
+
<type>(<optional-scope>)!: <description> ← breaking change
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Allowed Types
|
|
43
|
+
|
|
44
|
+
| Type | Purpose |
|
|
45
|
+
|------|---------|
|
|
46
|
+
| `feat` | New feature |
|
|
47
|
+
| `fix` | Bug fix |
|
|
48
|
+
| `docs` | Documentation only |
|
|
49
|
+
| `refactor` | Code refactoring (no behavior change) |
|
|
50
|
+
| `chore` | Maintenance, dependencies |
|
|
51
|
+
| `style` | Formatting, whitespace |
|
|
52
|
+
| `perf` | Performance improvement |
|
|
53
|
+
| `test` | Adding or fixing tests |
|
|
54
|
+
| `build` | Build system changes |
|
|
55
|
+
| `ci` | CI/CD pipeline changes |
|
|
56
|
+
| `revert` | Revert a previous commit |
|
|
57
|
+
|
|
58
|
+
### Rules
|
|
59
|
+
- Type MUST be lowercase, one of the listed values
|
|
60
|
+
- Scope is optional, lowercase, allows `a-z`, `0-9`, `.`, `_`, `-`
|
|
61
|
+
- `!` before `:` marks a breaking change
|
|
62
|
+
- Description MUST start after `: ` (colon + space)
|
|
63
|
+
- Description should be imperative mood ("add", not "added" or "adds")
|
|
64
|
+
|
|
65
|
+
### Valid Examples
|
|
66
|
+
```
|
|
67
|
+
feat(cli): add --json flag to session list command
|
|
68
|
+
fix(store): prevent duplicate observation insert on retry
|
|
69
|
+
docs(contributing): update workflow documentation
|
|
70
|
+
refactor(internal): extract search query sanitizer
|
|
71
|
+
chore(deps): bump github.com/charmbracelet/bubbletea to v0.26
|
|
72
|
+
style(tui): fix alignment in session detail view
|
|
73
|
+
perf(store): optimize FTS5 query for large datasets
|
|
74
|
+
test(sync): add coverage for conflict resolution
|
|
75
|
+
ci(workflows): split e2e into separate job
|
|
76
|
+
fix!: change session ID format
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Invalid Examples (push will be rejected)
|
|
80
|
+
```
|
|
81
|
+
Fix bug ← no type prefix
|
|
82
|
+
feat: Add login ← description should be lowercase
|
|
83
|
+
FEAT(cli): add flag ← type must be lowercase
|
|
84
|
+
feat (cli): add flag ← no space before scope
|
|
85
|
+
feat(CLI): add flag ← scope must be lowercase
|
|
86
|
+
update docs ← no conventional commit format
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Branch Naming Format (enforced by GitHub ruleset)
|
|
92
|
+
|
|
93
|
+
**Regex:** `^(feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)\/[a-z0-9._-]+$`
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
<type>/<description>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Allowed Types
|
|
100
|
+
|
|
101
|
+
`feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `revert`
|
|
102
|
+
|
|
103
|
+
### Rules
|
|
104
|
+
- Type MUST be one of the listed values
|
|
105
|
+
- Description MUST be lowercase
|
|
106
|
+
- Only `a-z`, `0-9`, `.`, `_`, `-` allowed in description
|
|
107
|
+
- No uppercase, no spaces, no special characters
|
|
108
|
+
|
|
109
|
+
### Valid Examples
|
|
110
|
+
```
|
|
111
|
+
feat/json-export-command
|
|
112
|
+
fix/duplicate-observation-insert
|
|
113
|
+
docs/api-reference-update
|
|
114
|
+
refactor/extract-query-sanitizer
|
|
115
|
+
chore/bump-bubbletea-v0.26
|
|
116
|
+
ci/split-e2e-job
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Invalid Examples (push will be rejected)
|
|
120
|
+
```
|
|
121
|
+
feature/add-login ← "feature" not allowed, use "feat"
|
|
122
|
+
fix/Add-Login ← uppercase not allowed
|
|
123
|
+
my-branch ← no type prefix
|
|
124
|
+
fix_something ← missing "/" separator
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Pre-Commit Checklist
|
|
130
|
+
|
|
131
|
+
- [ ] Commit message matches conventional commits format
|
|
132
|
+
- [ ] Branch name matches `type/description` format
|
|
133
|
+
- [ ] Diff matches commit scope (no unrelated changes)
|
|
134
|
+
- [ ] No secrets, credentials, or `.env` files
|
|
135
|
+
- [ ] Every changed path complies with the [Transient Artifact Policy](../../CONTRIBUTING.md#transient-artifact-policy)
|
|
136
|
+
- [ ] No `Co-Authored-By` trailers
|
|
137
|
+
- [ ] Tests relevant to the change pass
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nest-base
|
|
3
|
+
description: Crea la base del proyecto NestJS de este harness (Node.js en su última LTS, NestJS en su última versión estable, pnpm, ESLint y Prettier configurados, y la estructura common/ de AGENTS.md). Úsala solo cuando el repositorio aún no tiene package.json y el humano pide inicializar o crear la base del backend.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: nest-base
|
|
7
|
+
|
|
8
|
+
Crea el esqueleto NestJS que exige `AGENTS.md`. Esta skill **instala dependencias y consulta la red**, así que solo se ejecuta cuando el humano lo pide expresamente.
|
|
9
|
+
|
|
10
|
+
Referencias: `AGENTS.md` §2 (stack y dependencias aprobadas), §4 (estructura y fronteras), §6 a §10 (BD, caché, auth, contrato HTTP y CORS, logging, health y monitor) y §13 (prohibiciones). Detalle de implementación: `harness/reference/configuracion.md` (variables) y `harness/reference/contrato-http.md` (envelope, errores, auth, CORS, caché, logging, health, monitor).
|
|
11
|
+
|
|
12
|
+
**Plantillas** (`harness/skills/nest-base/templates/`): se copian tal cual al proyecto, sin cambiar su intención. Cada archivo indica su destino en la primera línea; quita ese comentario al copiarlo.
|
|
13
|
+
|
|
14
|
+
**Raíz del proyecto (`ROOT`):** la carpeta que **contiene** `harness/`, no `harness/` ni el directorio actual. Antes de empezar, haz `cd` a `ROOT`; todas las rutas de esta skill son relativas a él. El proyecto NestJS se crea en `ROOT`, nunca dentro de `harness/`.
|
|
15
|
+
|
|
16
|
+
## 0. Precondiciones: si alguna falla, detente e informa
|
|
17
|
+
|
|
18
|
+
1. `./harness/init.sh` termina en 0. Sin `package.json` corre en modo "solo harness".
|
|
19
|
+
2. **No existe `package.json`.** Si existe, la base ya fue creada: no la regeneres; informa y termina.
|
|
20
|
+
3. `pnpm` está disponible (`pnpm -v`). Nunca uses npm ni yarn.
|
|
21
|
+
|
|
22
|
+
## 1. Resolver versiones (siempre las actuales, nunca de memoria)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Node.js: última LTS publicada
|
|
26
|
+
curl -fsSL https://nodejs.org/dist/index.json \
|
|
27
|
+
| 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)})'
|
|
28
|
+
|
|
29
|
+
# NestJS: última versión estable (dist-tag "latest", nunca next/beta/rc)
|
|
30
|
+
pnpm view @nestjs/cli version
|
|
31
|
+
pnpm view @nestjs/core version
|
|
32
|
+
|
|
33
|
+
# pnpm: última estable
|
|
34
|
+
pnpm view pnpm version
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- NestJS no publica una línea "LTS". En este harness, "última LTS de NestJS" significa **la última versión estable del dist-tag `latest`**.
|
|
38
|
+
- Si el major de `node -v` no coincide con la LTS resuelta, **detente**. Pide al humano que la instale (por ejemplo `nvm install --lts` o `fnm install --lts`). No instales Node por tu cuenta.
|
|
39
|
+
- Informa al humano las versiones resueltas antes de continuar.
|
|
40
|
+
|
|
41
|
+
## 2. Generar el proyecto sin pisar el harness
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# ROOT = primera carpeta hacia arriba que contiene harness/AGENTS.md
|
|
45
|
+
ROOT="$(pwd)"; while [ "$ROOT" != "/" ] && [ ! -f "$ROOT/harness/AGENTS.md" ]; do ROOT="$(dirname "$ROOT")"; done
|
|
46
|
+
[ -f "$ROOT/harness/AGENTS.md" ] || { echo "No se encontró harness/AGENTS.md"; exit 1; }
|
|
47
|
+
cd "$ROOT"
|
|
48
|
+
TMP="$(mktemp -d)"
|
|
49
|
+
pnpm dlx @nestjs/cli@<version-cli> new <nombre-paquete> \
|
|
50
|
+
--package-manager pnpm --strict --skip-git --directory "$TMP/app"
|
|
51
|
+
rsync -a --ignore-existing --exclude node_modules "$TMP/app/" "$ROOT/"
|
|
52
|
+
rm -rf "$TMP"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- `<nombre-paquete>`: el nombre del directorio del repo en kebab-case, salvo que el humano indique otro.
|
|
56
|
+
- `--ignore-existing` protege `harness/` y los puentes de la raíz (`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.cursor/`, `.claude/` y `.agents/`).
|
|
57
|
+
- Elimina el ejemplo de Nest (`app.controller.ts`, `app.service.ts`, su spec y `test/app.e2e-spec.ts`).
|
|
58
|
+
- Reemplaza el `README.md` generado por uno breve en español que apunte a `AGENTS.md` y a `./harness/init.sh`.
|
|
59
|
+
|
|
60
|
+
## 3. Dependencias (solo las aprobadas en AGENTS.md §2)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pnpm install
|
|
64
|
+
pnpm add @nestjs/config @nestjs/jwt @nestjs/terminus pg ioredis \
|
|
65
|
+
class-validator class-transformer nestjs-pino pino-http react react-dom
|
|
66
|
+
pnpm add -D @types/pg @types/react @types/react-dom pino-pretty
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- Sin versiones fijadas a mano: pnpm resuelve la última estable y el lockfile las congela.
|
|
70
|
+
- `react` y `react-dom` son solo para el monitor (`src/common/monitor/`); no se usan en ningún otro lugar.
|
|
71
|
+
- `pino-pretty` va **solo en devDependencies**: formato legible de logs en desarrollo (`LOG_FORMAT=pretty`).
|
|
72
|
+
- Si pnpm avisa que ignoró scripts de build (`pnpm approve-builds`), muestra la lista al humano y espera su aprobación.
|
|
73
|
+
- Agrega el harness como devDependency desde npm, con la versión exacta del manifiesto:
|
|
74
|
+
```bash
|
|
75
|
+
pnpm add -D "@bi-analytics/arness-back@$(node -p "require('./harness/.arness.json').version")"
|
|
76
|
+
```
|
|
77
|
+
Si falla (red o registro), detente e informa; no lo quites de la lista.
|
|
78
|
+
- Cualquier paquete adicional requiere aprobación explícita y una decisión en `harness/decisiones/`.
|
|
79
|
+
|
|
80
|
+
## 4. Toolchain, ESLint y Prettier
|
|
81
|
+
|
|
82
|
+
1. **Node fijado:** crea `.nvmrc` con la versión LTS resuelta (ej. `v24.11.0`). En `package.json` define:
|
|
83
|
+
- `"engines": { "node": ">=<major>" }`
|
|
84
|
+
- `"packageManager": "pnpm@<version>"`
|
|
85
|
+
2. **Scripts de `package.json`:** agrega o ajusta estos. `init.sh` depende de `lint:check`, `format:check`, `build`, `test` y `test:e2e`.
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"lint": "eslint \"{src,test}/**/*.ts\" --fix",
|
|
89
|
+
"lint:check": "eslint \"{src,test}/**/*.ts\" --max-warnings 0",
|
|
90
|
+
"format": "prettier --write \"{src,test}/**/*.ts\"",
|
|
91
|
+
"format:check": "prettier --check \"{src,test}/**/*.ts\"",
|
|
92
|
+
"verify": "./harness/init.sh",
|
|
93
|
+
"harness:status": "arness status",
|
|
94
|
+
"harness:sync": "arness sync"
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
3. **ESLint:** reemplaza `eslint.config.mjs` con `harness/skills/nest-base/templates/eslint.config.mjs` y copia `.../templates/eslint.boundaries.mjs` a la raíz (la importa `eslint.config.mjs`). Si la versión generada por Nest usa una API distinta (por ejemplo `defineConfig`), conserva su base y porta el bloque de reglas del harness y los overrides por archivo sin cambiar su intención:
|
|
98
|
+
- no `any`;
|
|
99
|
+
- no `console`;
|
|
100
|
+
- no promesas flotantes;
|
|
101
|
+
- `pg` solo en `common/database` y `*.repository.ts`;
|
|
102
|
+
- texto SQL solo en `*.sql.ts` (también tras saltos de línea o `(`, e incluidas escrituras);
|
|
103
|
+
- nada de escrituras o DDL en `*.sql.ts`;
|
|
104
|
+
- fronteras (`eslint.boundaries.mjs`): `src/common` no importa de `src/modules`, y un módulo no importa de otro módulo ni de otro cliente; solo sale hacia `src/common` y `src/config`. Usa `@typescript-eslint/no-restricted-imports`, separada de la regla de `pg`, sin dependencias nuevas;
|
|
105
|
+
- configuración (`configBoundaries` de `eslint.boundaries.mjs`): `process.env` solo en `src/config/`, salvo specs y `test/`.
|
|
106
|
+
4. **Prettier:** copia `harness/skills/nest-base/templates/prettierrc.json` → `.prettierrc` y `.../templates/prettierignore` → `.prettierignore`.
|
|
107
|
+
5. **Editor:** copia `harness/skills/nest-base/templates/editorconfig` → `.editorconfig`.
|
|
108
|
+
6. **Aislar el harness del proyecto:**
|
|
109
|
+
- `tsconfig.json` y `tsconfig.build.json`: agrega `"harness"` a `exclude`.
|
|
110
|
+
- `.dockerignore`: incluye `harness`, `.agents`, `.claude`, `.cursor`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.env`, `.env.local`, `node_modules`, `dist` y `coverage`.
|
|
111
|
+
- `.prettierignore`: agrega `harness`.
|
|
112
|
+
7. **`.gitignore`:** asegúrate de que incluye `dist/`, `coverage/`, `node_modules/`, `logs/`, `.env`, `.env.local`, `.env.*.local` y `harness/progress.md`.
|
|
113
|
+
8. **Convención SQL:** las palabras clave SQL se escriben en MAYÚSCULAS. Así las detectan la regla de ESLint y `init.sh`.
|
|
114
|
+
|
|
115
|
+
## 5. Estructura base (AGENTS.md §4)
|
|
116
|
+
|
|
117
|
+
Implementa cada pieza con su spec. Sin lógica de negocio y sin módulos de ejemplo: `src/modules/` queda vacío con un `.gitkeep`.
|
|
118
|
+
|
|
119
|
+
| Ruta | Contenido mínimo |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `src/main.ts` | Logger pino (`bufferLogs`), prefijo `api/v1` excluyendo `health` y `monitor` (`{ exclude: ['health', 'monitor', 'monitor/{*path}'] }`, o la sintaxis de rutas de la versión de Nest), `ValidationPipe({ whitelist, forbidNonWhitelisted, transform })`, `app.enableCors(buildCorsOptions(...))` con los valores validados de config, `app.use(securityHeaders())` antes de las rutas, `app.set('trust proxy', hops)` solo si `app.trustProxyHops > 0` (`NestExpressApplication`), `enableShutdownHooks`, puerto desde `ConfigService` (`app.port`), nunca de `process.env` |
|
|
122
|
+
| `src/app.module.ts` | Importa config, logging, database, cache, auth, health; registra el guard JWT global y el filtro de errores |
|
|
123
|
+
| `src/config/` | `ConfigModule` global con `envFilePath: ['.env.local', '.env']`, `validate` y `load: [configuration]`; validación con class-validator que falla al arrancar, incluida la validación dinámica de `DB_<NOMBRE>_*` por cada nombre en `DATABASES`. `configuration.ts` y `configuration.spec.ts` desde `templates/config/`: **único lugar que lee `process.env`**, exporta `AppConfig` y no da valor por defecto a ningún secreto. `auth.ts` y `auth.spec.ts` desde `templates/config/`: `configuration.ts` llama a `parseAuthConfig` (secretos obligatorios, límite de intentos y vida de la sesión). `cors.ts` y `cors.spec.ts` desde `templates/cors/`: la validación y `configuration.ts` llaman a `parseCorsOrigins` y `parseCorsMaxAge`, así que la app no arranca sin `CORS_ORIGINS` o con `*`. El resto del código consume `ConfigService<AppConfig, true>` |
|
|
124
|
+
| `src/common/database/` | `DatabaseModule` dinámico: un `Pool` por nombre, `getPoolToken(name)`, sesión con `default_transaction_read_only=on` y `statement_timeout`, cierre en shutdown |
|
|
125
|
+
| `src/common/cache/` | `CacheService` sobre ioredis (`lazyConnect`, `enableOfflineQueue: false`, `commandTimeout`, reintentos con backoff). No lanza nunca y devuelve `HIT` / `MISS` / `BYPASS`. Hash estable de filtros (claves ordenadas → sha256). Con `CACHE_ENABLED=false` no conecta |
|
|
126
|
+
| `src/common/security/` | Copia `templates/security/` completo (con sus specs) y registra `SecurityModule` (global) en `app.module.ts`: `LoginAttemptLimiter`, `securityHeaders()` y `sessionClaims` / `isSessionExpired` (contrato-http.md, Autenticación y Cabeceras de seguridad) |
|
|
127
|
+
| `src/common/auth/` | `POST auth/login` y `POST auth/refresh` (`@Public()`), `timingSafeEqual` sobre sha256 de ambos valores, secretos distintos y claim `type`, `JwtAuthGuard` como `APP_GUARD`. Login con `LoginAttemptLimiter` por `req.ip` (`429 TOO_MANY_ATTEMPTS` con `Retry-After`). El par lleva `auth_time` (`sessionClaims`): el login lo fija y el refresh lo copia; `isSessionExpired` → `401 SESSION_EXPIRED`. El guard omite las rutas con `@Public()` y con `IS_MONITOR_ROUTE_KEY` (`@MonitorRoute()`, de `src/common/monitor/monitor.decorators.ts`) |
|
|
128
|
+
| `src/common/http/` | Tipos `{ data, meta? }`, filtro global de errores `{ error: { code, message, details?, requestId, path, timestamp } }`. Errores de pg: `57014` → 504 `QUERY_TIMEOUT`; errores de conexión o `08*` / `57P01` → 503 `DATABASE_UNAVAILABLE` |
|
|
129
|
+
| `src/common/logging/` | `logger-options.ts` y su spec desde `templates/logging/`; `LoggerModule.forRootAsync` con `buildLoggerParams(config.get('logging', { infer: true }))`. JSON por defecto, `pretty` en desarrollo con `LOG_FORMAT`, `genReqId` desde `X-Request-Id` seguro o UUID y devuelto en la respuesta, `redact` de authorization, cookies, password, tokens y secretos |
|
|
130
|
+
| `src/common/health/` | `GET /health` público con terminus. Indicadores `db_<nombre en minúsculas>` por conexión y `cache` con estado `up` / `down` / `disabled`; la caché nunca vuelve el health a 503 |
|
|
131
|
+
| `src/common/monitor/` | Copia `templates/monitor/` completo (con sus specs) y registra `MonitorModule` en `app.module.ts`. `GET /monitor` fuera de `/api/v1`, `404` si `MONITOR_ENABLED` no es `true`, login con `ACCESS_TOKEN` y cookie de sesión, cabeceras CSP sin scripts, widget base `endpoints-total` (contrato-http.md, Monitor) |
|
|
132
|
+
| `test/` | `auth.e2e-spec.ts` (login OK, login inválido, refresh, 401 sin token, `429` con `Retry-After` tras `AUTH_LOGIN_MAX_ATTEMPTS` fallos, `401 SESSION_EXPIRED` al refrescar pasada la vida máxima, refresh token y token del monitor como `Bearer` → 401, access token en `/auth/refresh` → 401, cabeceras de seguridad y sin `X-Powered-By`), `health.e2e-spec.ts` (pools y Redis mockeados con `overrideProvider`), y `cors.e2e-spec.ts` y `monitor.e2e-spec.ts` desde `templates/e2e/` |
|
|
133
|
+
|
|
134
|
+
### Archivos de soporte
|
|
135
|
+
|
|
136
|
+
- **`.env.example` y `.env.local`:** las mismas claves. Incluyen `NODE_ENV=development`, `PORT=3000`, `DATABASES=MAIN`, `DB_MAIN_*`, `ACCESS_TOKEN=example123`, `JWT_*`, `AUTH_SESSION_MAX_AGE_SECONDS=43200`, `AUTH_LOGIN_MAX_ATTEMPTS=5`, `AUTH_LOGIN_WINDOW_SECONDS=900`, `HTTP_TRUST_PROXY_HOPS=0`, `CACHE_*`, `REDIS_URL`, `LOG_LEVEL`, `LOG_FORMAT` (`json` en `.env.example`, `pretty` en `.env.local`), `CORS_ORIGINS=http://localhost:4200`, `CORS_MAX_AGE_SECONDS=600`, `MONITOR_ENABLED=false` y `MONITOR_SESSION_EXPIRES_IN=15m`. En `.env.example` todos los secretos llevan valores de ejemplo. **No crees ni toques `.env`.**
|
|
137
|
+
- **`postman/arness-back.postman_collection.json`** (formato v2.1): variables `baseUrl`, `accessToken` y `refreshToken`, y auth Bearer heredada. Carpeta `Auth` con Login (su script de test guarda los tokens) y Refresh, más la request `Health`. Agrega también `postman/local.postman_environment.json` con `baseUrl=http://localhost:3000`.
|
|
138
|
+
- **`FRONT.md`:**
|
|
139
|
+
- flujo de auth (login, uso del Bearer, refresh ante `401 TOKEN_EXPIRED`, volver al login ante `401 SESSION_EXPIRED`, y ante `429` esperar lo que indica `Retry-After`);
|
|
140
|
+
- envelope `{ data, meta? }` y formato de error;
|
|
141
|
+
- significado de `X-Cache`;
|
|
142
|
+
- CORS: el origen del frontend debe estar en `CORS_ORIGINS` de cada entorno (pedirlo al backend);
|
|
143
|
+
- endpoints de Auth con su consumo recomendado.
|
|
144
|
+
- **`docs/`:** copia `templates/docs/` (`TECNICO.md`, `EVOLUTIVO.md` y `PENDIENTES.md`). Completa en `TECNICO.md` lo que ya se sabe (cliente, conexión `MAIN`, entornos) y deja en `EVOLUTIVO.md` la entrada "Base del proyecto" con la fecha y hora de Bogotá y el responsable (formato de la plantilla). Los pendientes del reporte final (§7) van a `PENDIENTES.md`.
|
|
145
|
+
- **`Dockerfile`:** multi-stage con `node:<major-lts>-alpine`, `corepack enable`, `pnpm install --frozen-lockfile`, build y una imagen final solo con `dist/` y las dependencias de producción; usuario no root y `HEALTHCHECK` sobre `/health`.
|
|
146
|
+
- **`azure-pipelines.yml`:** Node desde `.nvmrc`, `corepack enable`, `pnpm install --frozen-lockfile`, `lint:check`, `format:check`, `build`, `test` y `docker build`.
|
|
147
|
+
|
|
148
|
+
## 6. Verificar (con protección contra bucles)
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pnpm lint && pnpm format
|
|
152
|
+
./harness/init.sh --strict
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
- Un proyecto nuevo nace cumpliendo todo: `./harness/init.sh --strict` debe terminar en 0, incluido `/health`. Los proyectos existentes usan el modo por defecto (advertencias). Si no hay PostgreSQL local disponible, el health informará las BDs caídas: repórtalo al humano y no lo "arregles" desactivando checks.
|
|
156
|
+
- Corrige solo fallos causados por archivos que esta skill creó. **Máximo 3 intentos de corrección.** Si sigue fallando, detente y reporta la salida de `init.sh`.
|
|
157
|
+
- Nunca relajes reglas de ESLint, umbrales ni checks de `init.sh` para pasar.
|
|
158
|
+
|
|
159
|
+
## 7. Reporte final
|
|
160
|
+
|
|
161
|
+
Informa:
|
|
162
|
+
- las versiones resueltas (Node LTS, NestJS, pnpm);
|
|
163
|
+
- los archivos creados;
|
|
164
|
+
- las dependencias instaladas;
|
|
165
|
+
- el resultado de `./harness/init.sh`;
|
|
166
|
+
- los pendientes (por ejemplo, credenciales reales de BD en `.env.local`).
|
|
167
|
+
|
|
168
|
+
No hagas commit salvo que el humano lo pida.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Plantilla del harness → src/config/auth.spec.ts
|
|
2
|
+
import { parseAuthConfig } from './auth';
|
|
3
|
+
|
|
4
|
+
const base = {
|
|
5
|
+
ACCESS_TOKEN: 'example123',
|
|
6
|
+
JWT_ACCESS_SECRET: 'access-secret',
|
|
7
|
+
JWT_REFRESH_SECRET: 'refresh-secret',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
describe('parseAuthConfig', () => {
|
|
11
|
+
it('applies non-sensitive defaults', () => {
|
|
12
|
+
const auth = parseAuthConfig(base);
|
|
13
|
+
expect(auth.jwt).toEqual({
|
|
14
|
+
accessSecret: 'access-secret',
|
|
15
|
+
accessExpiresIn: '15m',
|
|
16
|
+
refreshSecret: 'refresh-secret',
|
|
17
|
+
refreshExpiresIn: '7d',
|
|
18
|
+
});
|
|
19
|
+
expect(auth.login).toEqual({ maxAttempts: 5, windowSeconds: 900 });
|
|
20
|
+
expect(auth.sessionMaxAgeSeconds).toBe(43200);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it.each([
|
|
24
|
+
[{ ACCESS_TOKEN: undefined }, /ACCESS_TOKEN is required/],
|
|
25
|
+
[{ JWT_REFRESH_SECRET: ' ' }, /JWT_REFRESH_SECRET is required/],
|
|
26
|
+
[{ AUTH_LOGIN_MAX_ATTEMPTS: '0' }, /AUTH_LOGIN_MAX_ATTEMPTS must be an integer/],
|
|
27
|
+
[{ AUTH_LOGIN_WINDOW_SECONDS: 'abc' }, /AUTH_LOGIN_WINDOW_SECONDS must be an integer/],
|
|
28
|
+
[{ AUTH_SESSION_MAX_AGE_SECONDS: '30' }, /AUTH_SESSION_MAX_AGE_SECONDS must be an integer/],
|
|
29
|
+
])('rejects %p', (overrides, message) => {
|
|
30
|
+
expect(() => parseAuthConfig({ ...base, ...overrides })).toThrow(message);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Plantilla del harness → src/config/auth.ts (AGENTS.md §8, harness/reference/configuracion.md "Autenticación").
|
|
2
|
+
// Auth block of AppConfig: secrets (required, no default), login attempt limit and session max age.
|
|
3
|
+
// Errors name the variable, never its value.
|
|
4
|
+
|
|
5
|
+
export const AUTH_DEFAULT_LOGIN_MAX_ATTEMPTS = 5;
|
|
6
|
+
export const AUTH_DEFAULT_LOGIN_WINDOW_SECONDS = 900;
|
|
7
|
+
export const AUTH_DEFAULT_SESSION_MAX_AGE_SECONDS = 43200;
|
|
8
|
+
|
|
9
|
+
export interface AuthConfig {
|
|
10
|
+
accessToken: string;
|
|
11
|
+
jwt: {
|
|
12
|
+
accessSecret: string;
|
|
13
|
+
accessExpiresIn: string;
|
|
14
|
+
refreshSecret: string;
|
|
15
|
+
refreshExpiresIn: string;
|
|
16
|
+
};
|
|
17
|
+
login: { maxAttempts: number; windowSeconds: number };
|
|
18
|
+
sessionMaxAgeSeconds: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Env = Record<string, string | undefined>;
|
|
22
|
+
|
|
23
|
+
function required(env: Env, name: string): string {
|
|
24
|
+
const value = env[name];
|
|
25
|
+
if (value === undefined || value.trim() === '') {
|
|
26
|
+
throw new Error(`${name} is required`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function intInRange(env: Env, name: string, fallback: number, min: number, max: number): number {
|
|
32
|
+
const raw = env[name];
|
|
33
|
+
if (raw === undefined || raw.trim() === '') return fallback;
|
|
34
|
+
const value = Number(raw);
|
|
35
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
36
|
+
throw new Error(`${name} must be an integer between ${min} and ${max}`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function text(env: Env, name: string, fallback: string): string {
|
|
42
|
+
const value = env[name];
|
|
43
|
+
return value === undefined || value.trim() === '' ? fallback : value.trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Pure: reads and validates the auth block. */
|
|
47
|
+
export function parseAuthConfig(env: Env): AuthConfig {
|
|
48
|
+
return {
|
|
49
|
+
accessToken: required(env, 'ACCESS_TOKEN'),
|
|
50
|
+
jwt: {
|
|
51
|
+
accessSecret: required(env, 'JWT_ACCESS_SECRET'),
|
|
52
|
+
accessExpiresIn: text(env, 'JWT_ACCESS_EXPIRES_IN', '15m'),
|
|
53
|
+
refreshSecret: required(env, 'JWT_REFRESH_SECRET'),
|
|
54
|
+
refreshExpiresIn: text(env, 'JWT_REFRESH_EXPIRES_IN', '7d'),
|
|
55
|
+
},
|
|
56
|
+
login: {
|
|
57
|
+
maxAttempts: intInRange(
|
|
58
|
+
env,
|
|
59
|
+
'AUTH_LOGIN_MAX_ATTEMPTS',
|
|
60
|
+
AUTH_DEFAULT_LOGIN_MAX_ATTEMPTS,
|
|
61
|
+
1,
|
|
62
|
+
100,
|
|
63
|
+
),
|
|
64
|
+
windowSeconds: intInRange(
|
|
65
|
+
env,
|
|
66
|
+
'AUTH_LOGIN_WINDOW_SECONDS',
|
|
67
|
+
AUTH_DEFAULT_LOGIN_WINDOW_SECONDS,
|
|
68
|
+
1,
|
|
69
|
+
86400,
|
|
70
|
+
),
|
|
71
|
+
},
|
|
72
|
+
sessionMaxAgeSeconds: intInRange(
|
|
73
|
+
env,
|
|
74
|
+
'AUTH_SESSION_MAX_AGE_SECONDS',
|
|
75
|
+
AUTH_DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
76
|
+
60,
|
|
77
|
+
2592000,
|
|
78
|
+
),
|
|
79
|
+
};
|
|
80
|
+
}
|