refactor: complete bootstrap of ARNES agent harness framework

- Add complete agent harness structure with 8 roles (leader, triager, architect, implementer, reviewer, security, qa, documenter)
- Implement strict workflow with 9 stages and mandatory gates
- Add comprehensive verification script and runtime status tracking
- Create artifact-based evidence system with contracts and schemas
- Add agent policy matrix with permissions and anti-cheat rules
- Include test suite (44 tests passing) and CI-ready structure
- Add documentation: README, HOWTO, CHECKPOINTS, templates
- Configure model routing policies and token-aware task assignment
- Add BDD/SDD specification guides and feature templates
- Include starter pack for quick project onboarding

All verification checks pass. Framework ready for production use.
This commit is contained in:
rikrdo
2026-05-17 23:25:35 +02:00
parent 622e5df382
commit 3ff9b70e4c
104 changed files with 8534 additions and 187 deletions

107
spec/bdd/README.md Normal file
View File

@@ -0,0 +1,107 @@
# BDD — Behavior Driven Development
## Índice
- [Overview](#overview)
- [Features](#features)
- [Step Definitions](#step-definitions)
---
## Overview
Este directorio contiene especificaciones BDD en formato Gherkin.
Los archivos `.feature` sirven como especificación ejecutable.
### naming conventions
```
features/
├── <domain>/
│ ├── <feature-name>.feature
│ └── <feature-name>.feature
└── common/
└── <common-feature>.feature
```
### tags permitidos
| Tag | Uso |
|-----|-----|
| `@F-XXX` | Link a feature del backlog |
| `@smoke` | Tests críticos (siempre ejecutar) |
| `@regression` | Tests de regresión |
| `@integration` | Tests de integración |
| `@e2e` | End-to-end tests |
| `@unit` | Tests unitarios |
| `@api` | Tests de API |
| `@ui` | Tests de interfaz |
---
## Features
Ver `spec/bdd/features/` para los archivos `.feature`.
---
## Step Definitions
Los step definitions deben estar en:
- Python: `features/steps/*.py`
- JS/TS: `features/step_definitions/*.ts`
- Go: `features/steps/*.go`
### Template (Python/Behave)
```python
"""Steps para login feature."""
from behave import given, when, then
@given('un usuario registrado con email "{email}" y password "{password}"')
def step_registered_user(context, email, password):
"""Crea usuario de prueba."""
pass
@when('el usuario ingresa su email "{email}"')
def step_enter_email(context, email):
"""Ingresa email en el formulario."""
pass
@when('ingresa password "{password}"')
def step_enter_password(context, password):
"""Ingresa password."""
pass
@then('el sistema muestra mensaje de error "{message}"')
def step_show_error(context, message):
"""Verifica mensaje de error."""
pass
```
---
## Ejecutar Tests
### Python (Behave)
```bash
behave features/
behave features/ --tags @smoke
behave features/ --tags ~@slow # exclude
```
### Node.js (Cucumber)
```bash
npx cucumber-js features/
npx cucumber-js features/ --tags "@smoke and @F-001"
```
---
## Checklist de Feature
- [ ] Feature documentado en Gherkin
- [ ] Todos los scenarios tienen Given/When/Then
- [ ] Tags `@F-XXX` presentes
- [ ] Step definitions implementados
- [ ] Tests ejecutables

View File

@@ -0,0 +1,58 @@
# Features BDD
Este directorio contiene los archivos `.feature` organizados por dominio.
## Estructura
```
features/
├── auth/
│ ├── login.feature
│ └── registration.feature
├── dashboard/
│ └── dashboard.feature
├── common/
│ ├── navigation.feature
│ └── error-handling.feature
└── README.md
```
## Tags comunes
Usar estos tags en todos los features:
| Tag | Descripción |
|-----|-------------|
| `@F-XXX` | Link a feature ID del backlog |
| `@smoke` | Test crítico |
| `@regression` | Regresión |
## Example
```gherkin
@F-001 @auth @smoke
Feature: Inicio de sesión
Como usuario registrado
Quiero iniciar sesión con mis credenciales
Para acceder a mi cuenta personal
@positive
Scenario: Login exitoso con credenciales válidas
Given un usuario con email "user@example.com" y password "Password123"
And el usuario no tiene sesión activa
When el usuario ingresa email "user@example.com"
And ingresa password "Password123"
And presiona el botón "Iniciar sesión"
Then el sistema redirige al dashboard
And muestra mensaje de bienvenida
@negative
Scenario: Login fallido con password incorrecto
Given un usuario con email "user@example.com" y password "Password123"
When el usuario ingresa email "user@example.com"
And ingresa password "WrongPassword"
And presiona el botón "Iniciar sesión"
Then el sistema muestra mensaje de error "Credenciales inválidas"
And permanece en la página de login
```

View File

@@ -0,0 +1,70 @@
@F-004 @auth @login
Feature: User Login
Background:
Given the user "alice@example.com" exists with password "SecurePass123!"
@positive
Scenario: Successful login with valid credentials
Given I have valid email "alice@example.com" and password "SecurePass123!"
When I attempt to login
Then I should receive an access token
And the access token should contain user_id claim
And the access token should contain email claim
And the access token should not be expired
@positive
Scenario: Login returns refresh token
Given I have valid credentials for "alice@example.com"
When I login successfully
Then I should receive a refresh token
And the refresh token should be different from access token
And the refresh token should have longer expiration
@positive
Scenario: Login email is case-insensitive
Given a user exists with email "bob@test.com" and password "TestPass99!"
When I login with email "BOB@TEST.COM" and password "TestPass99!"
Then login should be successful
@negative
Scenario: Login with wrong password
Given I have email "alice@example.com" and password "WrongPassword123!"
When I attempt to login
Then I should receive error "Credenciales inválidas"
And I should not receive any token
@negative
Scenario: Login with nonexistent user
Given I have email "nonexistent@test.com" and password "AnyPass123!"
When I attempt to login
Then I should receive error "Credenciales inválidas"
And I should not receive any token
@negative
Scenario: Login with empty password
Given I have email "alice@example.com" and empty password
When I attempt to login
Then I should receive validation error
And I should not receive any token
@negative
Scenario: Login with invalid email format
Given I have email "not-an-email" and password "ValidPass123!"
When I attempt to login
Then I should receive validation error
And I should not receive any token
@security @rate-limit
Scenario: Login blocked after 10 failed attempts
Given I have email "alice@example.com" and password "WrongPassword!"
When I attempt to login 10 times with wrong password
Then account should be temporarily locked
And next login attempt should return error "Cuenta bloqueada"
@smoke
Scenario: Login endpoint responds with JSON
Given I have valid credentials for "alice@example.com"
When I send a POST request to "/api/v1/auth/login"
Then response should be JSON format
And response should have correct content-type header

View File

@@ -0,0 +1,58 @@
@F-004 @auth @logout
Feature: User Logout
Background:
Given the user "alice@example.com" exists with password "SecurePass123!"
And I am authenticated as "alice@example.com"
@positive
Scenario: Successful logout invalidates current session
Given my current access token is valid
When I logout
Then I should receive confirmation
And my session should be marked as revoked
And my access token should no longer be valid
@positive
Scenario: Logout with refresh token also invalidates access
Given I have a valid refresh token
When I logout
Then both access and refresh tokens should be invalid
And I should not be able to get new access token with refresh
@positive
Scenario: Logout all sessions for user
Given I am logged in from device "desktop"
And I am logged in from device "mobile"
When I logout from all devices
Then all my sessions should be invalidated
And I should not be able to use any previous token
@negative
Scenario: Using token after logout returns unauthorized
Given I previously logged in successfully
And I have logged out
When I try to use my old access token
Then I should receive 401 Unauthorized
And I should not have access to protected resources
@negative
Scenario: Logout with invalid token does nothing
Given I have an invalid/expired token
When I attempt to logout
Then logout should not fail
But no session should be affected
@security
Scenario: Concurrent logout requests are handled correctly
Given my session is valid
When I send multiple logout requests simultaneously
Then only one logout operation should occur
And token should be invalidated only once
@smoke
Scenario: Logout endpoint returns 200 on success
Given I am authenticated as "alice@example.com"
When I send POST request to "/api/v1/auth/logout"
Then response should be 200 OK
And response should indicate success

View File

@@ -0,0 +1,36 @@
# Common Features
Features que se reutilizan en múltiples dominios.
## Navigation
```gherkin
@common @navigation
Feature: Navegación entre páginas
Scenario: Navegar a través del menú
Given el usuario está en la página principal
When hace clic en el elemento de menú "Dashboard"
Then la URL cambia a "/dashboard"
And el título de la página muestra "Dashboard"
```
## Error Handling
```gherkin
@common @error-handling
Feature: Manejo de errores
Scenario: Mostrar error de red
Given la conexión a internet está disponible
And el servidor no responde
When el usuario realiza una acción que requiere red
Then el sistema muestra toast "Error de conexión"
And ofrece opción de reintentar
Scenario: Timeout de solicitud
Given el usuario tiene sesión activa
When realiza una solicitud que excede 30 segundos
Then el sistema muestra indicador de carga
And después de timeout muestra error "Solicitud expirada"
```

View File

@@ -0,0 +1,171 @@
@F-003 @password
Feature: Cambio de Contraseña
Como usuario autenticado
Quiero cambiar mi contraseña
Para mantener mi cuenta segura con credenciales actualizadas
# ====================
# HAPPY PATH
# ====================
@smoke @positive
Scenario: Cambiar contraseña exitosamente
Given un usuario autenticado con email "user@example.com"
And su contraseña actual es "OldPass123!"
When el usuario solicita cambiar contraseña
And ingresa contraseña actual "OldPass123!"
And ingresa nueva contraseña "NewPass456@"
And confirma nueva contraseña "NewPass456@"
Then el sistema valida la contraseña actual correctamente
And guarda la nueva contraseña hasheada
And invalida todas las sesiones existentes
And muestra mensaje de confirmación "Contraseña actualizada exitosamente"
@positive
Scenario: Contraseña con todos los caracteres especiales permitidos
Given un usuario autenticado
When cambia contraseña a "!@#$%^&*()_+-=[]{}|;':\",./<>?abc123ABC"
Then el sistema acepta la contraseña
And la guarda correctamente
# ====================
# PASSWORD VALIDATION
# ====================
@negative
Scenario: Nueva contraseña muy corta (menos de 8 caracteres)
Given un usuario autenticado
When intenta cambiar contraseña a "Ab1!"
Then el sistema muestra error "La contraseña debe tener al menos 8 caracteres"
And la contraseña no es cambiada
@negative
Scenario: Nueva contraseña muy larga (más de 128 caracteres)
Given un usuario autenticado
When intenta cambiar contraseña a "A" repetido 129 veces más "a1!"
Then el sistema muestra error "La contraseña debe tener máximo 128 caracteres"
And la contraseña no es cambiada
@negative
Scenario: Nueva contraseña sin mayúscula
Given un usuario autenticado
When intenta cambiar contraseña a "password123!"
Then el sistema muestra error "La contraseña debe contener al menos una mayúscula"
And la contraseña no es cambiada
@negative
Scenario: Nueva contraseña sin minúscula
Given un usuario autenticado
When intenta cambiar contraseña a "PASSWORD123!"
Then el sistema muestra error "La contraseña debe contener al menos una minúscula"
And la contraseña no es cambiada
@negative
Scenario: Nueva contraseña sin número
Given un usuario autenticado
When intenta cambiar contraseña a "PasswordABC!"
Then el sistema muestra error "La contraseña debe contener al menos un número"
And la contraseña no es cambiada
@negative
Scenario: Nueva contraseña sin carácter especial
Given un usuario autenticado
When intenta cambiar contraseña a "Password123"
Then el sistema muestra error "La contraseña debe contener al menos un carácter especial (!@#$%^&*...)"
And la contraseña no es cambiada
# ====================
# CURRENT PASSWORD
# ====================
@negative
Scenario: Contraseña actual incorrecta
Given un usuario autenticado con contraseña actual "CorrectPass123!"
When intenta cambiar contraseña con actual "WrongPass456!"
And nueva contraseña "NewPass789@"
Then el sistema muestra error "La contraseña actual es incorrecta"
And la contraseña no es cambiada
And no se invalidan sesiones
@negative
Scenario: Contraseña actual vacía
Given un usuario autenticado
When intenta cambiar contraseña con actual ""
And nueva contraseña "NewPass123@"
Then el sistema muestra error "La contraseña actual es requerida"
And la contraseña no es cambiada
# ====================
# PASSWORD MISMATCH
# ====================
@negative
Scenario: Nueva contraseña y confirmación no coinciden
Given un usuario autenticado
When ingresa contraseña actual correcta
And ingresa nueva contraseña "NewPass123@"
But confirma con "DifferentPass456!"
Then el sistema muestra error "Las contraseñas no coinciden"
And la contraseña no es cambiada
# ====================
# REUSE DETECTION
# ====================
@negative @security
Scenario: Reutilizar contraseña anterior
Given un usuario autenticado con contraseña actual "MyPass123!"
And historial de contraseñas incluye "MyPass123!"
When intenta cambiar contraseña a "MyPass123!"
Then el sistema muestra error "La nueva contraseña no puede ser igual a la anterior"
And la contraseña no es cambiada
# ====================
# AUTHORIZATION
# ====================
@negative @security
Scenario: Usuario no autenticado intenta cambiar contraseña
Given un usuario no autenticado
When intenta cambiar contraseña
Then el sistema retorna error 401 "No autorizado"
And la contraseña no es cambiada
@negative @security
Scenario: Token expirado al cambiar contraseña
Given un usuario con sesión expirada
When intenta cambiar contraseña
Then el sistema retorna error 401 "Sesión expirada"
And la contraseña no es cambiada
@negative @security
Scenario: Intentar cambiar contraseña de otro usuario
Given un usuario autenticado con ID "user-123"
When intenta cambiar contraseña del usuario "user-456"
Then el sistema retorna error 403 "No tienes permiso para modificar esta cuenta"
And la contraseña no es cambiada
# ====================
# RATE LIMITING
# ====================
@negative @security
Scenario: Superar límite de intentos (rate limit)
Given un usuario autenticado
And ya realizó 5 intentos fallidos en la última hora
When intenta cambiar contraseña una vez más
Then el sistema retorna error 429 "Demasiados intentos. Intenta de nuevo en 1 hora"
And todas las solicitudes son bloqueadas hasta que pase el tiempo
# ====================
# SUCCESSFUL REAUTHENTICATION
# ====================
@positive
Scenario: Cambio de contraseña seguido de login exitoso
Given un usuario con contraseña "OldPass123!"
When cambia su contraseña a "NewPass456@"
And luego intenta iniciar sesión con "NewPass456@"
Then el login es exitoso
And el usuario accede a su cuenta

View File

@@ -0,0 +1,159 @@
@F-002 @profile
Feature: Gestión de Perfil de Usuario
Como usuario autenticado
Quiero gestionar mi perfil
Para mantener mis datos personales actualizados y personalizar mi experiencia
# ====================
# VIEW PROFILE
# ====================
@smoke @positive
Scenario: Ver perfil de usuario exitosamente
Given un usuario autenticado con ID "user-123" y nombre "Juan Pérez"
And el usuario tiene avatar "https://cdn.example.com/avatar-123.jpg"
And el idioma configurado es "es"
When el usuario solicita ver su perfil
Then el sistema retorna los datos completos del perfil
And incluye id "user-123", nombre "Juan Pérez"
And incluye avatar_url y language "es"
@negative
Scenario: Ver perfil sin autenticación
Given un usuario no autenticado
When el usuario solicita ver su perfil
Then el sistema retorna error 401 "No autorizado"
And no retorna datos del perfil
@negative
Scenario: Ver perfil de usuario inexistente
Given un usuario autenticado
When solicita ver perfil de ID "nonexistent-user"
Then el sistema retorna error 404 "Usuario no encontrado"
# ====================
# UPDATE NAME
# ====================
@smoke @positive
Scenario: Editar nombre del perfil exitosamente
Given un usuario autenticado con ID "user-123"
And el perfil tiene nombre "Juan"
When el usuario actualiza su nombre a "Pedro"
Then el perfil muestra nombre "Pedro"
And la fecha de updated_at se actualiza
@positive
Scenario: Editar nombre con caracteres unicode válidos
Given un usuario autenticado
When cambia su nombre a "José García"
Then el sistema acepta el cambio
And el nombre se guarda como "José García"
@negative
Scenario: Editar nombre con caracteres inválidos
Given un usuario autenticado
When intenta cambiar nombre a "Juan@123!"
Then el sistema muestra error de validación "Nombre inválido: solo letras y espacios"
And el nombre permanece sin cambios
@negative
Scenario: Editar nombre con menos de 2 caracteres
Given un usuario autenticado
When intenta cambiar nombre a "J"
Then el sistema muestra error "Nombre debe tener al menos 2 caracteres"
@negative
Scenario: Editar nombre con más de 50 caracteres
Given un usuario autenticado
When intenta cambiar nombre a "A" repetido 51 veces
Then el sistema muestra error "Nombre debe tener máximo 50 caracteres"
# ====================
# UPDATE AVATAR
# ====================
@smoke @positive
Scenario: Cambiar avatar exitosamente
Given un usuario autenticado con avatar actual "https://cdn.example.com/old.jpg"
When el usuario sube un nuevo avatar "https://cdn.example.com/new.jpg"
Then el perfil muestra avatar_url "https://cdn.example.com/new.jpg"
@negative
Scenario: Cambiar avatar con URL inválida
Given un usuario autenticado
When intenta cambiar avatar a "not-a-valid-url"
Then el sistema muestra error "URL de avatar inválida"
And el avatar permanece sin cambios
@negative
Scenario: Cambiar avatar con URL de protocolo no permitido
Given un usuario autenticado
When intenta cambiar avatar a "ftp://malicious.com/file.jpg"
Then el sistema muestra error "Solo se permiten URLs http o https"
And el avatar permanece sin cambios
# ====================
# UPDATE LANGUAGE
# ====================
@smoke @positive
Scenario: Cambiar idioma a español exitosamente
Given un usuario autenticado con idioma "en"
When el usuario cambia idioma a "es"
Then el idioma se guarda como "es"
And el sistema confirma el cambio
@positive
Scenario: Cambiar idioma a francés
Given un usuario autenticado
When cambia idioma a "fr"
Then el sistema acepta "fr" como idioma válido
@positive
Scenario: Cambiar idioma a alemán
Given un usuario autenticado
When cambia idioma a "de"
Then el sistema acepta "de" como idioma válido
@negative
Scenario: Cambiar idioma a idioma no soportado
Given un usuario autenticado
When intenta cambiar idioma a "zh"
Then el sistema muestra error "Idioma no soportado"
And el idioma permanece sin cambios
# ====================
# PARTIAL UPDATE
# ====================
@positive
Scenario: Actualizar solo nombre sin cambiar avatar
Given un usuario autenticado con nombre "Juan" y avatar "https://cdn.com/img.jpg"
When el usuario solo actualiza nombre a "Pedro"
Then el nombre cambia a "Pedro"
And el avatar_url permanece "https://cdn.com/img.jpg"
@positive
Scenario: Actualizar múltiples campos en una petición
Given un usuario autenticado
When envía actualización con nombre "María", avatar "https://cdn.com/maria.jpg", idioma "es"
Then todos los campos se actualizan correctamente
And el perfil refleja todos los cambios
# ====================
# AUTHORIZATION
# ====================
@negative @security
Scenario: Usuario intenta editar perfil de otro usuario
Given un usuario autenticado con ID "user-123"
When intenta actualizar perfil de usuario "user-456"
Then el sistema retorna error 403 "No tienes permiso para editar este perfil"
@negative @security
Scenario: Token expirado al editar perfil
Given un usuario con token expirado
When intenta actualizar su perfil
Then el sistema retorna error 401 "Sesión expirada"

303
spec/sdd-bdd-guide.md Normal file
View File

@@ -0,0 +1,303 @@
# SDD/BBD Guide — System Design Document & Behavior Driven Development
Guía para crear y mantener SDD (System Design Document) y BDD (Behavior Driven Development) specs dentro del framework ARNES.
---
## 📐 Propósito
- **SDD**: Documenta el diseño técnico del sistema (arquitectura, componentes, decisiones).
- **BDD**: Documenta el comportamiento esperado desde la perspectiva del usuario/negocio.
Ambos alimentan el pipeline de agentes y se versionan junto con el código.
---
## 🔗 Relación con ARNES
```
spec/
├── product.md # Qué construir (negocio)
├── tech.md # Stack y decisiones técnicas
├── acceptance.md # Criterios de aceptación (BDD light)
├── sdd/ # System Design Document
│ ├── README.md
│ ├── architecture.md
│ ├── components/
│ └── decisions/
└── bdd/ # Behavior Driven Development
├── README.md
├── features/
└── step_definitions/
```
---
## 📋 SDD — System Design Document
### Objetivos
1. Definir arquitectura del sistema
2. Documentar componentes y sus responsabilidades
3. Registrar decisiones técnicas (ADRs)
4. Servir como fuente de verdad para `architect` y `implementer`
### Estructura de un SDD
```
spec/sdd/
├── README.md # Índice y overview
├── architecture.md # Vista general (contexto, capas)
├── components/ # Componentes individuales
│ ├── component-name.md
│ └── ...
└── decisions/ # Architecture Decision Records
├── 001-decision-title.md
└── ...
```
### Template: component.md
```markdown
# Componente: <Nombre>
## Responsabilidad
Descripción clara de qué hace este componente.
## Interfaces
- **Entrada**: API, eventos, mensajes
- **Salida**: Respuestas, side effects
## Dependencias
- Servicio A (tipo de dependencia)
- Base de datos B
## Límites
- Qué NO hace este componente
- Restricciones conocidas
## Criterios de éxito
- [ ] Mecanismo de verificación
- [ ] Métrica de performance
```
### Template: ADR (Architecture Decision Record)
```markdown
# ADR-XXX: <Título>
## Estado
Aceptado | Deprecado | Propuesto
## Contexto
Problema que motiva esta decisión.
## Decisión
Qué se decidió y por qué.
## Consecuencias
- ✅ Positivos
- ❌ Negativos
## Alternativas consideradas
1. Opción A - razón de descarte
2. Opción B - razón de descarte
## Fecha
YYYY-MM-DD
```
---
## 🎯 BDD — Behavior Driven Development
### Objetivos
1. Definir comportamiento del sistema en lenguaje de negocio
2. Crear trazabilidad entre requisitos y tests
3. Servir como especificación ejecutable para `implementer` y `qa`
### Formato: Gherkin
Usar sintaxis Gherkin para todos los features:
```gherkin
Feature: <Nombre del feature>
Como <actor>
Quiero <acción>
Para <beneficio>
Scenario: <escenario positivo>
Given <contexto inicial>
And <más contexto>
When <acción del usuario>
And <otra acción>
Then <resultado esperado>
And <otro resultado>
Scenario: <escenario negativo>
Given <contexto>
When <acción que falla>
Then <error esperado>
```
### Estructura de un Feature BDD
```
spec/bdd/features/
├── README.md
├── auth/
│ ├── login.feature
│ └── registration.feature
├── checkout/
│ └── purchase.feature
└── common/
└── error-handling.feature
```
### Tags para trazabilidad
```gherkin
@F-001 @auth @smoke
Feature: Inicio de sesión
@regression @slow
Scenario: Login con credenciales válidas
...
```
Tags disponibles:
- `@F-XXX` — Link a feature ID del backlog
- `@smoke` — Test crítico (ejecutar siempre)
- `@regression` — Tests de regresión
- `@integration` — Tests de integración
- `@e2e` — End-to-end tests
---
## 🔄 Flujo de trabajo SDD/BDD en ARNES
### Stage: design (architect)
1. **Crear/actualizar SDD**
- Definir componentes nuevos
- Documentar decisiones técnicas
- Crear ADRs cuando haya cambios
2. **Crear/actualizar BDD**
- Traducir requisitos de `spec/product.md` a Gherkin
- Crear scenarios para cada criterio de aceptación
- Asegurar que cada scenario tenga link a `@F-XXX`
3. **Producir artefacto**
- Archivo: `work/artifacts/<feature_id>/architect.md`
- Contenido: resumen de cambios en SDD/BDD
### Stage: build (implementer)
1. **Implementar código** que cumpla los scenarios BDD
2. **Escribir tests** que ejecuten los scenarios
3. **Actualizar SDD** si hay cambios en componentes
### Stage: review_gate (reviewer)
1. **Verificar** que el código implementa lo documentado en SDD
2. **Verificar** que tests cubren los scenarios BDD
3. **Producir** `work/artifacts/<feature_id>/reviewer.json`
### Stage: qa_gate (qa)
1. **Ejecutar** tests BDD (feature files)
2. **Verificar** trazabilidad: todos los `@F-XXX` tienen tests
3. **Producir** `work/artifacts/<feature_id>/qa.json`
---
## 🛠 Herramientas recomendadas
| Propósito | Herramienta | Notas |
|-----------|-------------|-------|
| BDD test runner | Behave (Python) / Cucumber (JS/Java) | Ejecuta .feature files |
| SDD docs | Markdown + Mermaid diagrams | Portable y versionable |
| ADRs |adr-tools o manual | Mantener en `decisions/` |
### Ejemplo: Python Behave
```bash
# Estructura
features/
├── login.feature
└── steps/
└── login_steps.py
# Ejecutar
behave features/
```
### Ejemplo: Node.js Cucumber
```bash
# Estructura
features/
├── login.feature
└── step_definitions/
└── login_steps.js
# Ejecutar
npx cucumber-js features/
```
---
## 📊 Checklist de calidad SDD/BDD
### SDD Quality
- [ ] Cada componente tiene responsabilidad clara
- [ ] Interfaces están documentadas
- [ ] ADRs para decisiones importantes
- [ ] Diagramas Mermaid para arquitectura
### BDD Quality
- [ ] Cada feature tiene al menos un scenario
- [ ] Todos los scenarios usan Given/When/Then
- [ ] Tags `@F-XXX` para trazabilidad con backlog
- [ ] Scenarios son atómicos (no dependen de estado previo)
---
## 🚫 Reglas anti-trampa
1. **SDD no es decoration**: debe reflejar la realidad del código
2. **BDD no es documentación de tests**: es especificación executable
3. **Discrepancia = bug**: si SDD dice A pero código hace B, el código está mal
4. **Sin scenario = sin acceptance**: feature sin BDD scenario no puede cerrarse
---
## 📝 Formato de artefacto architect.md
```markdown
# Architect Artefact — Feature: F-XXX
## SDD Changes
- Componentes afectados: [...]
- ADRs creados/actualizados: [...]
## BDD Coverage
- Features/Scenarios nuevos: [...]
- Coverage: X/Y scenarios cubiertos por tests
## Decisiones técnicas
- Decisión 1: razón
- Decisión 2: razón
## Riesgos identificados
- Riesgo 1: mitigación
```
---
## 🔗 Referencias
- [Gherkin Reference](https://cucumber.io/docs/gherkin/)
- [MADR (Markdown Any Decision Records)](https://adr.github.io/madr/)
- [BDD with Behave](https://behave.readthedocs.io/)

67
spec/sdd/README.md Normal file
View File

@@ -0,0 +1,67 @@
# SDD — System Design Document
## Índice
- [Architecture Overview](#architecture-overview)
- [Components](#components)
- [Decisions](#decisions)
---
## Architecture Overview
```mermaid
graph TD
subgraph Frontend
F[Client App]
end
subgraph Backend
A[API Gateway]
S[Services]
D[(Database)]
end
F --> A
A --> S
S --> D
```
### Contexto
_Describir el propósito del sistema y su alcance._
### Restricciones
- _Lista de restricciones técnicas o de negocio_
---
## Components
### Component Template
Ver `spec/sdd/components/.template.md` para el formato.
---
## Decisions
Ver `spec/sdd/decisions/` para ADRs.
---
## Diagrama de secuencia (ejemplo)
```mermaid
sequenceDiagram
actor U as User
participant API
participant SVC as Service
participant DB as Database
U->>API: Request
API->>SVC: Process
SVC->>DB: Query
DB-->>SVC: Result
SVC-->>API: Response
API-->>U: Data
```

View File

@@ -0,0 +1,74 @@
# Component: <Nombre>
## Responsabilidad
Descripción clara de qué hace este componente.
## Tipo
- [ ] Microservicio
- [ ] Library/Biblioteca
- [ ] Shared Component
- [ ] External Integration
## Interfaces
### API (si aplica)
```
Method: GET/POST/PUT/DELETE /endpoint
Input: { ... }
Output: { ... }
Errors: 400, 401, 404, 500
```
### Eventos (si aplica)
- `topic.name.v1` — descripción del evento
## Dependencias
| Servicio/Biblioteca | Tipo | Notas |
|---------------------|------|-------|
| | | |
## Límites
### Alcance
- ✅ Qué hace
- ❌ Qué NO hace
### Constraints
- Timeout máximo: Xms
- Rate limit: Y req/min
## Criterios de éxito
| Criterio | Métrica | Target |
|----------|---------|--------|
| Disponibilidad | uptime | 99.9% |
| Latencia | p99 | < 200ms |
## Diagrama
```mermaid
graph LR
A[Input] --> B[Component]
B --> C[Output]
```
## Estados
| Estado | Trigger | Acción |
|--------|---------|--------|
| Initial | created | ... |
| Active | running | ... |
| Error | failure | ... |
## Seguridad
- Authentication: ...
- Authorization: ...
- Rate limiting: ...
## Observabilidad
- Metrics: ...
- Logs: ...
- Traces: ...

View File

@@ -0,0 +1,65 @@
# AuthService Component
## Purpose
Handle user authentication (login/logout) with JWT tokens.
## Public API
### Methods
#### login(email: str, password: str) -> AuthResult
Authenticate user with email and password.
**Parameters:**
- `email`: User email address
- `password`: User password
**Returns:**
- `AuthResult` with access_token, refresh_token, expires_in
**Raises:**
- `InvalidCredentialsError`: Email or password incorrect
- `AccountLockedError`: Account temporarily locked
- `ValidationError`: Invalid input format
#### logout(user_id: str, token_id: str) -> bool
Invalidate a specific session/token.
**Parameters:**
- `user_id`: User ID
- `token_id`: JWT jti (token identifier)
**Returns:** True if successful
#### logout_all(user_id: str) -> int
Invalidate all sessions for a user.
**Parameters:**
- `user_id`: User ID
**Returns:** Number of sessions invalidated
#### refresh(refresh_token: str) -> AuthResult
Get new access token from refresh token.
**Parameters:**
- `refresh_token`: Valid refresh token
**Returns:** New AuthResult with access_token
**Raises:**
- `InvalidTokenError`: Token expired or invalid
---
## Dependencies
- `TokenService`: JWT generation/validation
- `SessionStore`: Track active sessions
- `UserRepository`: Fetch user data
- `PasswordService`: Verify password (from F-003)
## Configuration
```python
LOGIN_RATE_LIMIT = 10 # attempts per window
RATE_LIMIT_WINDOW = 900 # 15 minutes
ACCOUNT_LOCKOUT_DURATION = 1800 # 30 minutes

View File

@@ -0,0 +1,114 @@
# Component: PasswordService
## Responsabilidad
Gestionar el cambio de contraseña de usuarios autenticados. Validar contraseña actual, verificar requisitos de seguridad de la nueva contraseña, y invalidar sesiones existentes.
## Tipo
- [x] Microservicio
- [ ] Library/Biblioteca
- [ ] Shared Component
- [ ] External Integration
## Interfaces
### API REST
```
POST /api/v1/users/{user_id}/change-password
Authorization: Bearer <token>
Input: {
"current_password": string,
"new_password": string,
"confirm_password": string
}
Output: { "success": true, "message": "Contraseña actualizada" }
Errors:
- 400: Validation errors (password too weak, mismatch)
- 401: Current password incorrect
- 403: Not owner
- 404: User not found
```
## Dependencias
| Servicio/Biblioteca | Tipo | Notas |
|---------------------|------|-------|
| PostgreSQL | Database | Almacenamiento de usuarios |
| Redis | Cache | Invalidation de sesiones |
| AuthService | Internal | Verificación de token |
## Validaciones de contraseña
| Regla | Requisito | Mensaje de error |
|-------|-----------|------------------|
| Longitud mínima | 8 caracteres | "La contraseña debe tener al menos 8 caracteres" |
| Longitud máxima | 128 caracteres | "La contraseña debe tener máximo 128 caracteres" |
| Mayúsculas | Al menos 1 | "La contraseña debe contener al menos una mayúscula" |
| Minúsculas | Al menos 1 | "La contraseña debe contener al menos una minúscula" |
| Números | Al menos 1 | "La contraseña debe contener al menos un número" |
| Caracteres especiales | Al menos 1 | "La contraseña debe contener al menos un carácter especial (!@#$%^&*...)" |
| No usar password anterior | Diferente | "La nueva contraseña no puede ser igual a la anterior" |
## Límites
### Alcance
- ✅ Cambio de contraseña con validación
- ✅ Requisitos de seguridad
- ✅ Invalidación de sesiones
- ❌ NO maneja recuperación de contraseña (ver ForgotPasswordService)
- ❌ NO maneja reset forzado por admin (ver AdminService)
### Constraints
- Rate limit: 5 intentos por hora por usuario
- Timeout máximo: 1 segundo
- Máximo 3 passwords válidas en historial (evitar reutilización inmediata)
## Criterios de éxito
| Criterio | Métrica | Target |
|----------|---------|--------|
| Disponibilidad | uptime | 99.9% |
| Latencia | p99 change_password | < 500ms |
| Rate limit | blocked attempts | 100% |
| Sesiones invalidées | después de cambio | 100% |
## Diagrama
```mermaid
graph LR
A[Client] -->|POST /change-password| B[PasswordService]
B -->|validate current| C[(PostgreSQL)]
B -->|validate new| D[PasswordValidator]
D -->|strong enough?| E{Valid}
E -->|yes| F[Hash + Save]
E -->|no| G[Return error]
F -->|invalidate| H[(Redis)]
H -->|remove sessions| I[All user tokens]
```
## Estados
| Estado | Trigger | Acción |
|--------|---------|--------|
| Initial | started | Connect to DB |
| Ready | db_connected | Accept requests |
| RateLimited | >5 attempts/hour | Return 429 |
| Error | db_failure | Return 503 |
## Seguridad
- **Password hashing**: bcrypt, cost 12 (nuevo), verificar contra hash existente
- **Timing attack prevention**: usar constant-time comparison
- **Rate limiting**: 5 req/hour por user_id
- **Sesiones**: invalidar TODAS las sesiones del usuario tras cambio
- **Logs**: NO registrar passwords, solo intentos fallidos (user_id anonymized)
## Observabilidad
- Metrics: `password_change_total`, `password_change_failed`, `password_change_latency`
- Logs: structured JSON con request_id
- Traces: OpenTelemetry span por request
## Tests BDD
- Ver `spec/bdd/features/password/change-password.feature`

View File

@@ -0,0 +1,75 @@
# SessionStore Component
## Purpose
Manage active user sessions in Redis for fast authentication and revocation.
## Public API
### Methods
#### create_session(user_id: str, token_id: str, metadata: dict) -> bool
Store a new active session.
**Parameters:**
- `user_id`: User identifier
- `token_id`: JWT jti (unique token ID)
- `metadata`: Optional data (IP, user agent, device)
**Returns:** True if created
#### get_session(token_id: str) -> Session | None
Retrieve active session info.
**Parameters:**
- `token_id`: JWT jti
**Returns:** Session object or None if expired/revoked
#### revoke_session(token_id: str) -> bool
Invalidate a specific session.
**Parameters:**
- `token_id`: JWT jti
**Returns:** True if revoked
#### revoke_all_user_sessions(user_id: str) -> int
Invalidate all sessions for a user.
**Parameters:**
- `user_id`: User identifier
**Returns:** Count of sessions revoked
#### get_user_session_count(user_id: str) -> int
Count active sessions for a user.
**Parameters:**
- `user_id`: User identifier
**Returns:** Number of active sessions
---
## Redis Keys Structure
```
session:{user_id}:{token_id} -> JSON session metadata
user_sessions:{user_id} -> SET of active token_ids
rate_limit:login:{ip} -> COUNT with TTL
```
## TTL
- Session tokens: 15 minutes (synced with access token)
- Rate limit counters: 15 minutes
## Dependencies
- Redis connection (via aioredis)
- TokenService (for token ID generation)
## Configuration
```python
SESSION_TTL = 900 # 15 minutes
MAX_SESSIONS_PER_USER = 10
RATE_LIMIT_WINDOW = 900 # 15 minutes
```

View File

@@ -0,0 +1,69 @@
# TokenService Component
## Purpose
Generate, validate, and manage JWT tokens.
## Public API
### Methods
#### create_access_token(user: User) -> str
Generate a new JWT access token.
**Parameters:**
- `user`: User object with id, email, role
**Returns:** JWT token string
**Token claims:**
```json
{
"sub": user.id,
"email": user.email,
"role": user.role,
"iat": current_timestamp,
"exp": current_timestamp + 900, # 15 min
"jti": uuid4()
}
```
#### create_refresh_token(user: User) -> str
Generate a new refresh token.
**Returns:** JWT refresh token (7 day expiration)
#### verify_token(token: str) -> TokenPayload
Validate and decode a JWT token.
**Parameters:**
- `token`: JWT token string
**Returns:** TokenPayload with claims
**Raises:**
- `ExpiredSignatureError`: Token expired
- `InvalidTokenError`: Token invalid/malformed
#### revoke_token(token_id: str, user_id: str) -> bool
Mark a token as revoked in session store.
**Parameters:**
- `token_id`: JWT jti claim
- `user_id`: User ID
**Returns:** True if revoked
---
## Configuration
```python
ACCESS_TOKEN_EXPIRE = 900 # 15 minutes
REFRESH_TOKEN_EXPIRE = 604800 # 7 days
ALGORITHM = "HS256" # or RS256 with key pair
SECRET_KEY = os.getenv("JWT_SECRET")
```
## Security
- Tokens include unique `jti` claim for revocation tracking
- Short access token duration minimizes theft window
- Refresh tokens stored in Redis for fast revocation

View File

@@ -0,0 +1,111 @@
# Component: UserProfileService
## Responsabilidad
Gestionar el perfil de usuario: consulta, actualización de datos básicos (nombre, avatar) y preferencias (idioma).
## Tipo
- [x] Microservicio
- [ ] Library/Biblioteca
- [ ] Shared Component
- [ ] External Integration
## Interfaces
### API REST
```
GET /api/v1/users/{user_id}/profile
Authorization: Bearer <token>
Output: {
"id": string,
"name": string,
"avatar_url": string,
"language": "en" | "es" | "fr" | "de",
"created_at": ISO8601,
"updated_at": ISO8601
}
Errors: 401 (unauthorized), 404 (user not found)
PUT /api/v1/users/{user_id}/profile
Authorization: Bearer <token>
Input: {
"name": string (optional),
"avatar_url": string (optional),
"language": string (optional)
}
Output: { perfil actualizado }
Errors: 400 (validation), 401, 403 (not owner), 404
```
### Eventos (si aplica)
- `profile.updated.v1` — publicado cuando perfil se actualiza
## Dependencias
| Servicio/Biblioteca | Tipo | Notas |
|---------------------|------|-------|
| PostgreSQL | Database | Datos de usuarios y perfiles |
| Redis | Cache | Cache de perfil (TTL 5min) |
| Storage Service | External | Almacenamiento de avatares |
## Límites
### Alcance
- ✅ CRUD de perfil de usuario
- ✅ Cambio de idioma
- ❌ NO maneja autenticación (AuthService)
- ❌ NO maneja permisos de otros usuarios
### Constraints
- Timeout máximo: 300ms
- Rate limit: 50 req/min por usuario
- name: 2-50 caracteres, solo letras y espacios
- avatar_url: max 500 caracteres, URL válida (http/https)
- language: uno de ['en', 'es', 'fr', 'de']
## Criterios de éxito
| Criterio | Métrica | Target |
|----------|---------|--------|
| Disponibilidad | uptime | 99.9% |
| Latencia | p99 get_profile | < 100ms |
| Latencia | p99 update_profile | < 200ms |
| Cache hit rate | | > 80% |
## Diagrama
```mermaid
graph LR
A[Client] -->|GET /profile| B[UserProfileService]
B -->|cache| C[(Redis)]
B -->|fetch| D[(PostgreSQL)]
E[Client] -->|PUT /profile| B
B -->|validate| F[Storage]
```
## Estados
| Estado | Trigger | Acción |
|--------|---------|--------|
| Initial | started | Connect to DB, Redis |
| Ready | all_connected | Accept requests |
| Degraded | redis_down | Fallback to DB-only |
| Error | db_failure | Return 503 + alert |
## Seguridad
- Authentication: JWT Bearer token required
- Authorization: Solo el dueño puede modificar su perfil
- Input validation: Pydantic, sanitización XSS
- Rate limiting: 50 req/min por user_id
## Observabilidad
- Metrics: `profile_get_total`, `profile_update_total`, `profile_latency_ms`
- Logs: structured JSON con user_id (masked)
- Traces: OpenTelemetry span por request
## Tests BDD
- Ver `spec/bdd/features/profile/user-profile.feature`

View File

@@ -0,0 +1,48 @@
# ADR-XXX: Título de la Decisión
## Estado
Aceptado | Propuesto | Deprecado
## Fecha
YYYY-MM-DD
## Contexto
_Descripción del problema o situación que motiva esta decisión._
## Decisión
_Qué se decidió y por qué._
## Justificación
_Razones que fundamentan la decisión._
## Consecuencias
### ✅ Positivas
- ...
### ❌ Negativas
- ...
### 🔄 Neutrales
- ...
## Alternativas Consideradas
### Opción A
- **Descripción**: ...
- **Pros**: ...
- **Contras**: ...
- **Razón de descarte**: ...
### Opción B
- **Descripción**: ...
- **Pros**: ...
- **Contras**: ...
- **Razón de descarte**: ...
## Notas
_Información adicional o follow-ups._
## Relacionado con
- ADR-YYY
- Feature F-XXX

View File

@@ -0,0 +1,63 @@
# ADR-001: Selección de Stack Técnico
## Estado
Aceptado
## Fecha
2026-05-06
## Contexto
Necesitamos seleccionar el stack tecnológico inicial para el proyecto. El equipo tiene experiencia en Python y JavaScript/TypeScript, y requiere:
- Rápido bootstrap
- Testing BDD nativo
- Compatibilidad con el framework ARNES
## Decisión
Usar **Python + Behave** para BDD y **FastAPI** para el backend.
## Justificación
1. **Behave** tiene sintaxis Gherkin nativa y integración simple con Python
2. **FastAPI** ofrece validación automática con Pydantic y tests con pytest
3. Ambos tienen ecosistema maduro y documentación extensa
4. Comunidad activa y soporte a largo plazo
## Consecuencias
### ✅ Positivas
- Curva de aprendizaje baja (Python)
- BDD nativo con Behave (Gherkin)
- Type hints en todo el stack
- FastAPI: auto-generated docs (Swagger/ReDoc)
- Testing integrado con pytest
### ❌ Negativas
- GIL限制了多线程性能 (puede mitigated with async)
- Menos opciones de hosting que Node.js
### 🔄 Neutrales
- Requiere Python 3.10+ mínimo
## Alternativas Consideradas
### Opción A: Node.js + Cucumber
- **Pros**: Más opciones de hosting, JSON nativo, ecosistema npm enorme
- **Contras**: TypeScript requiere más setup, testing E2E más complejo
- **Razón de descarte**: Mayor complejidad inicial, menor familiaridad del equipo con TS
### Opción B: Java + Cucumber-JVM
- **Pros**: Tipo estático, robusto, enterprise-grade
- **Contras**: Verbose, setup pesado, curva de aprendizaje alta
- **Razón de descarte**: Over-engineering para MVP
### Opción C: Go + Godog
- **Pros**: Binarios estáticos, excelente performance
- **Contras**: BDD tooling inmaduro, less ecosystem para testing
- **Razón de descarte**: BDD ecosystem no maduro
## Notas
- Re-evaluar si el proyecto escala a más de 50 servicios
- Considerar microservices framework si es necesario
## Relacionado con
- Feature F-001
- Stack: Python 3.11+, FastAPI, Behave, PostgreSQL

View File

@@ -0,0 +1,69 @@
# ADR-002: Almacenamiento de Avatares
## Estado
Aceptado
## Fecha
2026-05-06
## Contexto
Los usuarios pueden subir avatares personalizados. Necesitamos decidir dónde y cómo almacenar las imágenes de perfil para optimizar costo, rendimiento y mantenimiento.
## Decisión
Usar **Storage Service externo (S3-compatible)** con URLs firmadas para avatares.
## Justificación
1. **Simplicidad**: No requerimos procesar imágenes en nuestro servidor
2. **Costo**: S3-like storage es económico ($0.023/GB)
3. **CDN**: Los avatares se sirven desde CDN automáticamente
4. **Seguridad**: URLs firmadas con expiración evitan hotlinking
5. **Mantenimiento**: No requiere gestión de sistema de archivos
## Consecuencias
### ✅ Positivas
- No hay infraestructura de archivos que mantener
- Escalabilidad automática
- URLs firmadas = más seguridad
- Cache CDN = mejor performance
### ❌ Negativas
- Dependencia de proveedor externo
- Costo de storage + egress
- Latencia extra por redirect a CDN
### 🔄 Neutrales
- Requiere configuración de CORS
## Alternativas Consideradas
### Opción A: Almacenamiento local en servidor
- **Pros**: Sin dependencia externa, rápido para lecturas
- **Contras**: No escala horizontalmente, requiere backup, problemas de disco
- **Razón de descarte**: No escala bien con múltiples instancias
### Opción B: Base de datos como BLOB
- **Pros**: Todo en un lugar, transacciones integradas
- **Contras**: PostgreSQL no optimizado para archivos grandes, backup lento
- **Razón de descarte**: degrada performance de DB, backups muy pesados
### Opción C: Servicio dedicado de imágenes (Cloudinary/Imgix)
- **Pros**: Transformación de imágenes, CDN incluido, optimización automática
- **Contras**: Más costoso ($50+/mes), vendor lock-in
- **Razón de descarte**: Over-engineering para avatares simples
## Implementación
1. Cliente sube imagen a `/api/v1/profile/upload` (multipart)
2. Servicio valida tipo (jpg/png/webp) y tamaño (<5MB)
3. Servicio sube a S3 con nombre `avatars/{user_id}/{timestamp}.{ext}`
4. Servicio genera URL firmada (7 días validez)
5. URL se guarda en campo `avatar_url` del perfil
## Notas
- Considerar WebP en el futuro para optimización
- Implementar cleanup de avatares huérfanos (job semanal)
## Relacionado con
- Feature F-002
- Componente: UserProfileService

View File

@@ -0,0 +1,83 @@
# ADR-003: Hashing de Contraseñas
## Estado
Aceptado
## Fecha
2026-05-06
## Contexto
Necesitamos guardar contraseñas de usuarios de forma segura. La decisión debe considerar:
- Resistencia a ataques de fuerza bruta y rainbow tables
- Performance (se ejecuta en cada login y cambio de password)
- Compatibilidad con estándares de la industria
## Decisión
Usar **bcrypt** con cost factor 12 para hashing de contraseñas.
## Justificación
1. **bcrypt** es diseñado específicamente para password hashing lento
2. **Cost factor configurable**: permite aumentar resistencia en el futuro
3. **Resistente a GPU/rainbow attacks**: diseñado para ser lento intencionalmente
4. **Incorpora salt**: cada password tiene salt único, evitando rainbow tables
5. **Estándar de industria**: ampliamente usado (Django, Rails, bcrypt)
## Consecuencias
### ✅ Positivas
- Resistente a ataques de fuerza bruta
- Salt automático evitar rainbow tables
- Configurable (cost factor)
- Librerías maduras en todos los lenguajes
### ❌ Negativas
- Más lento que MD5/SHA (es el punto, pero afecta latency)
- Enorme payload si se guarda en cookies/token
### 🔄 Neutrales
- Requiere Python 3.11+ para bcrypt moderno
## Implementación
```python
import bcrypt
def hash_password(password: str) -> str:
"""Hash password with bcrypt, cost 12."""
return bcrypt.hashpw(
password.encode('utf-8'),
bcrypt.gensalt(rounds=12)
).decode('utf-8')
def verify_password(password: str, hashed: str) -> bool:
"""Verify password using constant-time comparison."""
return bcrypt.checkpw(
password.encode('utf-8'),
hashed.encode('utf-8')
)
```
## Alternativas Consideradas
### Opción A: SHA-256 (con salt)
- **Pros**: Rápido, simple
- **Contras**: No es lento, vulnerable a GPU attacks, diseñado para speed no security
- **Razón de descarte**: No es resistente a hardware moderno
### Opción B: Argon2
- **Pros**: Ganador PHC 2015, configurable memory/CPU
- **Contras**: Más complejo de implementar, menos soporte de librerías
- **Razón de descarte**: bcrypt es más simple y suficiente para nuestro caso de uso
### Opción C: scrypt
- **Pros**: Diseñado para ser memory-hard
- **Contras**: Más lento de configurar, configuración compleja
- **Razón de descarte**: bcrypt es más simple y ampliamente soportado
## Notas
- Si en el futuro我们需要 mayor seguridad, migrar a Argon2
- No guardar passwords en logs bajo ninguna circunstancia
## Relacionado con
- Feature F-003
- Componente: PasswordService

View File

@@ -0,0 +1,68 @@
# ADR-004: JWT Authentication Strategy
## Status
ACCEPTED
## Context
We need a stateless authentication mechanism for the API that:
1. Allows users to login with email/password
2. Provides secure token-based sessions
3. Supports token revocation (logout)
4. Handles token refresh without re-login
## Decision
We will use **JWT (JSON Web Tokens)** with the following configuration:
### Token Structure
- **Access Token**: 15 minute expiration, contains user identity
- **Refresh Token**: 7 day expiration, used to obtain new access tokens
### Algorithm
- **HS256** for signing (symmetric, simpler setup)
- Secret key loaded from environment variable `JWT_SECRET`
### Claims
```json
{
"sub": "user_uuid",
"email": "user@example.com",
"role": "user",
"iat": 1715030400,
"exp": 1715031300,
"jti": "unique-token-id"
}
```
### Session Management
- Active sessions tracked in **Redis** (keyed by `jti`)
- Sessions invalidated on logout
- All user sessions invalidated on password change (from F-003)
## Consequences
### Positive
- Stateless = horizontal scaling friendly
- Short-lived access tokens limit damage if compromised
- Refresh tokens allow long sessions without storing passwords
- Redis-based session tracking enables instant revocation
### Negative
- Cannot revoke individual refresh tokens (need blocklist)
- Token size larger than session IDs
- Clock sync required between services
## Alternatives Considered
| Alternative | Why Rejected |
|-------------|--------------|
| Session cookies | Not API-friendly, CSRF issues |
| OAuth2/OIDC | Overkill for simple auth |
| PASETO | Less battle-tested |
| opaque tokens | Requires DB lookup on every request |
## Implementation Notes
- JWT library: PyJWT
- Redis client: aioredis for async
- Both tokens stored in HttpOnly cookies for browser clients
- Access token in Authorization header for API clients