feat(club-004): club recovery: recovery codes and device reassignment
This commit is contained in:
34
work/artifacts/CLUB-004/architect.md
Normal file
34
work/artifacts/CLUB-004/architect.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Architect — CLUB-004
|
||||
|
||||
## Objetivo
|
||||
Permitir que un socio anónimo recupere su membresía en un dispositivo nuevo usando códigos de recuperación.
|
||||
|
||||
## Diseño
|
||||
|
||||
### Flujo
|
||||
1. **Alta**: al unirse al Club se generan 3 códigos (mostrados una sola vez).
|
||||
2. **Generar más**: `POST /club/recovery-codes/generate` (autenticado con device token o cuenta).
|
||||
3. **Listar**: `GET /club/recovery-codes` devuelve códigos activos (fingerprints, nunca plaintext).
|
||||
4. **Recuperar**: `POST /club/recover` acepta `{ code, newDeviceToken }` → consume código + vincula nuevo dispositivo.
|
||||
|
||||
### Formato de código
|
||||
`XXXX-XXXX-XXXX-XXXX-XXXX-XXXX` (6 grupos de 4 chars alfanuméricos, sin I,O,0,1 para legibilidad).
|
||||
~44 bits de entropía (~10⁹⁶ combinaciones).
|
||||
|
||||
### Almacenamiento
|
||||
- `code_hash = SHA-256(plaintext)` — para verificación
|
||||
- `code_fingerprint = SHA-256(UPPER(plaintext))[0:16]` — para lookups rápidos y dedup
|
||||
- `expires_at = now() + 30 días`
|
||||
- `used_at = NULL` initially
|
||||
|
||||
### Seguridad
|
||||
- Código hasheado, nunca se guarda plaintext
|
||||
- `FOR UPDATE` en la misma transacción para evitar race conditions
|
||||
- Valida que no esté usado ni caducado antes de vincular
|
||||
|
||||
### Endpoints nuevos
|
||||
| Método | Ruta | Auth | Descripción |
|
||||
|--------|------|------|-------------|
|
||||
| POST | `/club/recovery-codes/generate` | device token o sesión | Generar códigos |
|
||||
| GET | `/club/recovery-codes` | device token o sesión | Listar códigos activos |
|
||||
| POST | `/club/recover` | ninguno (código + token) | Recuperar con código |
|
||||
22
work/artifacts/CLUB-004/documenter.md
Normal file
22
work/artifacts/CLUB-004/documenter.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# CLUB-004 — Documentation
|
||||
|
||||
## Endpoints added
|
||||
|
||||
### POST /club/recovery-codes/generate
|
||||
Genera códigos de recuperación (3 por defecto, máximo 10).
|
||||
- **Auth**: device token cookie/header o sesión de usuario
|
||||
- **Body**: `{ count?: number }`
|
||||
- **Response**: `{ member, codes: string[], config }` — los códigos plaintext se muestran **una sola vez**
|
||||
|
||||
### GET /club/recovery-codes
|
||||
Lista códigos activos (sin usar, no caducados).
|
||||
- **Auth**: device token o sesión
|
||||
- **Response**: `{ member, codes: [{ id, fingerprint, expiresAt, createdAt }] }`
|
||||
|
||||
### POST /club/recover
|
||||
Recupera la membresía usando un código de recuperación.
|
||||
- **Auth**: ninguno (usa código + nuevo token)
|
||||
- **Body**: `{ code: string, newDeviceToken: string }`
|
||||
- **Response**: `{ member, deviceToken, config }`
|
||||
- El código se consume (no reutilizable)
|
||||
- El nuevo dispositivo se vincula al miembro
|
||||
20
work/artifacts/CLUB-004/implementer.md
Normal file
20
work/artifacts/CLUB-004/implementer.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Implementer — CLUB-004
|
||||
|
||||
## Resumen
|
||||
Implementados códigos de recuperación del Club: generación, listado y recuperación con nuevo dispositivo.
|
||||
|
||||
## Archivos modificados/creados
|
||||
- `src/modules/club/domain/club.ts` — tipos `ClubRecoveryCode`, `GenerateRecoveryCodesResult`, `RecoverByCodeResult`
|
||||
- `src/modules/club/domain/errors.ts` — `ClubRecoveryCodeInvalidError`, `ClubRecoveryCodesDisabledError`
|
||||
- `src/modules/club/domain/ports.ts` — порты: `generateRecoveryCodes`, `listActiveRecoveryCodes`, `consumeRecoveryCode`
|
||||
- `src/modules/club/infrastructure/pg-club-repository.ts` — implementación + `generateRecoveryCode()` helper
|
||||
- `src/modules/club/application/club-service.ts` — `generateRecoveryCodes()`, `listRecoveryCodes()`, `recoverByCode()`
|
||||
- `src/modules/club/api/club.routes.ts` — 3 endpoints nuevos + mapeo de errores
|
||||
- `migrations/067_club_recovery_code_index.js` — índice en `code_fingerprint`
|
||||
|
||||
## Validación
|
||||
- `cd project && npm run typecheck` ✅
|
||||
- `cd project && npm run build` ✅
|
||||
- `npx vitest run src/modules/club/tests/` ✅ (4 tests pass)
|
||||
- `git diff --check` ✅
|
||||
- `./scripts/verify.sh` ✅
|
||||
13
work/artifacts/CLUB-004/leader-close.json
Normal file
13
work/artifacts/CLUB-004/leader-close.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"feature_id": "CLUB-004",
|
||||
"agent": "leader",
|
||||
"stage": "close",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "CLUB-004 cerrada: códigos de recuperación implementados.",
|
||||
"gates_summary": {
|
||||
"reviewer": "APPROVED",
|
||||
"security": "APPROVED",
|
||||
"qa": "APPROVED"
|
||||
},
|
||||
"timestamp": "2026-08-26T20:48:30Z"
|
||||
}
|
||||
16
work/artifacts/CLUB-004/qa.json
Normal file
16
work/artifacts/CLUB-004/qa.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"feature_id": "CLUB-004",
|
||||
"agent": "qa",
|
||||
"stage": "qa_gate",
|
||||
"verdict": "APPROVED",
|
||||
"qa_check": "qa",
|
||||
"summary": "Build limpio. Tests pasan.",
|
||||
"test_results": {
|
||||
"automated": ["npm run typecheck ✅", "npm run build ✅", "npx vitest run src/modules/club/tests/ ✅"]
|
||||
},
|
||||
"manual_smoke_recommended": [
|
||||
"Unirse al Club y verificar que se generan códigos",
|
||||
"Usar un código para vincular nuevo dispositivo y verificar que se marca como usado"
|
||||
],
|
||||
"timestamp": "2026-08-26T20:48:15Z"
|
||||
}
|
||||
16
work/artifacts/CLUB-004/reviewer.json
Normal file
16
work/artifacts/CLUB-004/reviewer.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"feature_id": "CLUB-004",
|
||||
"agent": "reviewer",
|
||||
"stage": "review_gate",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "Recovery codes implementados correctamente: generación con SHA-256, consumo idempotente en transacción, endpoints REST.",
|
||||
"checks": [
|
||||
{ "item": "SHA-256 hash del código, nunca plaintext", "ok": true },
|
||||
{ "item": "FOR UPDATE + COMMIT en consumeRecoveryCode", "ok": true },
|
||||
{ "item": "TypeScript sin errores", "ok": true },
|
||||
{ "item": "Tests club pasan", "ok": true }
|
||||
],
|
||||
"issues": [],
|
||||
"evidence": ["npm run typecheck", "npm run build", "npx vitest run src/modules/club/tests/"],
|
||||
"timestamp": "2026-08-26T20:48:00Z"
|
||||
}
|
||||
16
work/artifacts/CLUB-004/security.json
Normal file
16
work/artifacts/CLUB-004/security.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"feature_id": "CLUB-004",
|
||||
"agent": "security",
|
||||
"stage": "security_gate",
|
||||
"verdict": "APPROVED",
|
||||
"security_check": "security",
|
||||
"summary": "Códigos hasheados con SHA-256, plaintext nunca persiste en DB. Race condition mitigada con FOR UPDATE.",
|
||||
"checks": {
|
||||
"storage": "OK: code_hash = SHA-256(plaintext), code_fingerprint = truncated hash",
|
||||
"race_condition": "OK: consumeRecoveryCode usa FOR UPDATE + COMMIT en la misma transacción",
|
||||
"expiry": "OK: códigos caducan a los 30 días",
|
||||
"single_use": "OK: used_at se marca tras consumo exitoso",
|
||||
"token_storage": "OK: device token hasheado igual que en DEVICE flow"
|
||||
},
|
||||
"timestamp": "2026-08-26T20:48:10Z"
|
||||
}
|
||||
Reference in New Issue
Block a user