- Add MetricsPage and useOnboarding hook - Update superadmin controller and service - Add module documentation (docs/01-modulos/) - Add CONTEXT-MAP.yml and Sprint 5 execution report - Update project status and task traces 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
245 lines
5.7 KiB
Markdown
245 lines
5.7 KiB
Markdown
# SAAS-010: Webhooks
|
|
|
|
## Metadata
|
|
- **Codigo:** SAAS-010
|
|
- **Modulo:** Webhooks
|
|
- **Prioridad:** P2
|
|
- **Estado:** Pendiente
|
|
- **Fase:** 5 - Integraciones
|
|
|
|
## Descripcion
|
|
|
|
Sistema de webhooks outbound: configuracion de endpoints por tenant, eventos suscribibles, firma de payloads, reintentos automaticos, y logs de entregas.
|
|
|
|
## Objetivos
|
|
|
|
1. Configuracion de webhooks por tenant
|
|
2. Eventos suscribibles
|
|
3. Firma HMAC de payloads
|
|
4. Reintentos con backoff
|
|
5. Dashboard de entregas
|
|
|
|
## Alcance
|
|
|
|
### Incluido
|
|
- CRUD de webhooks por tenant
|
|
- Eventos de sistema suscribibles
|
|
- Firma HMAC-SHA256
|
|
- Reintentos exponenciales (max 5)
|
|
- Logs de entregas
|
|
- Test endpoint
|
|
|
|
### Excluido
|
|
- Transformacion de payloads
|
|
- Webhooks inbound (recibir)
|
|
- Fanout a multiples endpoints por evento
|
|
|
|
## Modelo de Datos
|
|
|
|
### Tablas (schema: webhooks)
|
|
|
|
**webhooks**
|
|
- id, tenant_id, name
|
|
- url, secret (encrypted)
|
|
- events (JSONB array)
|
|
- headers (JSONB)
|
|
- is_active, created_at
|
|
|
|
**webhook_deliveries**
|
|
- id, webhook_id, event_type
|
|
- payload (JSONB)
|
|
- response_status, response_body
|
|
- attempt, next_retry_at
|
|
- delivered_at, created_at
|
|
|
|
## Eventos Disponibles
|
|
|
|
| Evento | Descripcion | Payload |
|
|
|--------|-------------|---------|
|
|
| user.created | Usuario creado | User object |
|
|
| user.updated | Usuario actualizado | User + changes |
|
|
| user.deleted | Usuario eliminado | { userId } |
|
|
| subscription.created | Nueva suscripcion | Subscription |
|
|
| subscription.updated | Suscripcion cambiada | Subscription |
|
|
| subscription.cancelled | Suscripcion cancelada | Subscription |
|
|
| invoice.paid | Factura pagada | Invoice |
|
|
| invoice.failed | Pago fallido | Invoice |
|
|
|
|
## Endpoints API
|
|
|
|
| Metodo | Endpoint | Descripcion |
|
|
|--------|----------|-------------|
|
|
| GET | /webhooks | Listar webhooks |
|
|
| GET | /webhooks/:id | Obtener webhook |
|
|
| POST | /webhooks | Crear webhook |
|
|
| PUT | /webhooks/:id | Actualizar webhook |
|
|
| DELETE | /webhooks/:id | Eliminar webhook |
|
|
| POST | /webhooks/:id/test | Enviar test |
|
|
| GET | /webhooks/:id/deliveries | Historial entregas |
|
|
| POST | /webhooks/:id/deliveries/:did/retry | Reintentar |
|
|
| GET | /webhooks/events | Eventos disponibles |
|
|
|
|
## Firma de Payloads
|
|
|
|
### Generacion
|
|
```typescript
|
|
function signPayload(payload: object, secret: string): string {
|
|
const timestamp = Date.now();
|
|
const body = JSON.stringify(payload);
|
|
const signature = crypto
|
|
.createHmac('sha256', secret)
|
|
.update(`${timestamp}.${body}`)
|
|
.digest('hex');
|
|
|
|
return `t=${timestamp},v1=${signature}`;
|
|
}
|
|
```
|
|
|
|
### Headers Enviados
|
|
```
|
|
X-Webhook-Signature: t=1704067200000,v1=abc123...
|
|
X-Webhook-Id: wh_123456
|
|
X-Webhook-Event: user.created
|
|
X-Webhook-Timestamp: 1704067200000
|
|
```
|
|
|
|
### Verificacion (lado receptor)
|
|
```typescript
|
|
function verifySignature(payload: string, signature: string, secret: string): boolean {
|
|
const [timestamp, hash] = parseSignature(signature);
|
|
|
|
// Verificar timestamp (5 min tolerance)
|
|
if (Date.now() - timestamp > 300000) return false;
|
|
|
|
const expected = crypto
|
|
.createHmac('sha256', secret)
|
|
.update(`${timestamp}.${payload}`)
|
|
.digest('hex');
|
|
|
|
return crypto.timingSafeEqual(
|
|
Buffer.from(hash),
|
|
Buffer.from(expected)
|
|
);
|
|
}
|
|
```
|
|
|
|
## Logica de Reintentos
|
|
|
|
```
|
|
Intento 1: Inmediato
|
|
Intento 2: +1 minuto
|
|
Intento 3: +5 minutos
|
|
Intento 4: +30 minutos
|
|
Intento 5: +2 horas
|
|
Despues: Marcar como fallido
|
|
```
|
|
|
|
### Status de Entrega
|
|
| Status | Descripcion |
|
|
|--------|-------------|
|
|
| pending | En cola |
|
|
| delivered | Entregado (2xx) |
|
|
| failed | Fallo permanente |
|
|
| retrying | En reintento |
|
|
|
|
## Implementacion
|
|
|
|
### Dispatcher
|
|
```typescript
|
|
@Injectable()
|
|
export class WebhookDispatcher {
|
|
async dispatch(tenantId: string, event: string, data: object): Promise<void> {
|
|
const webhooks = await this.getActiveWebhooks(tenantId, event);
|
|
|
|
for (const webhook of webhooks) {
|
|
await this.webhookQueue.add('deliver', {
|
|
webhookId: webhook.id,
|
|
event,
|
|
payload: data,
|
|
attempt: 1
|
|
});
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Worker
|
|
```typescript
|
|
@Processor('webhooks')
|
|
export class WebhookWorker {
|
|
@Process('deliver')
|
|
async deliver(job: Job<WebhookJob>): Promise<void> {
|
|
const { webhookId, event, payload, attempt } = job.data;
|
|
const webhook = await this.getWebhook(webhookId);
|
|
|
|
const signature = this.signPayload(payload, webhook.secret);
|
|
|
|
try {
|
|
const response = await axios.post(webhook.url, payload, {
|
|
headers: {
|
|
'X-Webhook-Signature': signature,
|
|
'X-Webhook-Event': event,
|
|
...webhook.headers
|
|
},
|
|
timeout: 30000
|
|
});
|
|
|
|
await this.logDelivery(webhookId, event, payload, response, 'delivered');
|
|
} catch (error) {
|
|
await this.handleFailure(job, error);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Limites por Plan
|
|
|
|
| Plan | Webhooks | Eventos/mes |
|
|
|------|----------|-------------|
|
|
| Free | 0 | 0 |
|
|
| Starter | 0 | 0 |
|
|
| Pro | 5 | 10,000 |
|
|
| Enterprise | 20 | 100,000 |
|
|
|
|
## Entregables
|
|
|
|
| Entregable | Estado | Archivo |
|
|
|------------|--------|---------|
|
|
| webhooks.module.ts | Pendiente | `modules/webhooks/` |
|
|
| webhook.service.ts | Pendiente | `services/` |
|
|
| webhook.dispatcher.ts | Pendiente | `services/` |
|
|
| webhook.worker.ts | Pendiente | `workers/` |
|
|
| DDL webhooks schema | Pendiente | `ddl/schemas/webhooks/` |
|
|
|
|
## Dependencias
|
|
|
|
### Depende de
|
|
- SAAS-002 (Tenants)
|
|
- SAAS-005 (Plans - feature flag)
|
|
- BullMQ para queue
|
|
|
|
### Bloquea a
|
|
- Integraciones de terceros
|
|
- Automatizaciones externas
|
|
|
|
## Criterios de Aceptacion
|
|
|
|
- [ ] CRUD webhooks funciona
|
|
- [ ] Eventos se disparan
|
|
- [ ] Firma es correcta
|
|
- [ ] Reintentos funcionan
|
|
- [ ] Test endpoint funciona
|
|
- [ ] Logs se registran
|
|
|
|
## Seguridad
|
|
|
|
- Secrets encriptados en BD
|
|
- HTTPS requerido para URLs
|
|
- Timeout de 30 segundos
|
|
- Rate limit por tenant
|
|
- No seguir redirects
|
|
|
|
---
|
|
|
|
**Ultima actualizacion:** 2026-01-07
|