Passo-a-passo detalhado do skill, referenciando as fases cognitivas:
1SENSE — Identificar provider e configuração
Verificar documentação: qual header tem a assinatura? (`X-Hub-Signature-256` no GitHub, `Stripe-Signature` no Stripe)
Configurar secret no dashboard do provider e armazenar em `WEBHOOK_SECRET`
2RECOMMEND — Implementar handler com validação HMAC
```typescript
// webhook.ts — Next.js App Router
import { createHmac, timingSafeEqual } from 'crypto';
export async function POST(req: Request) {
const rawBody = await req.text(); // CRÍTICO: raw body, não JSON parsed
const signature = req.headers.get('x-hub-signature-256') ?? '';
// Validação HMAC timing-safe
const expected = createHmac('sha256', process.env.WEBHOOK_SECRET!)
.update(rawBody).digest('hex');
const expectedHeader = `sha256=${expected}`;
if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expectedHeader))) {
return new Response('Unauthorized', { status: 401 });
}
// Anti-replay: verificar timestamp
const payload = JSON.parse(rawBody);
const eventTime = payload.timestamp ?? Date.now() / 1000;
if (Math.abs(Date.now() / 1000 - eventTime) > 300) {
return new Response('Request too old', { status: 400 });
}
// Idempotência: verificar se já processado
const eventId = payload.id ?? req.headers.get('x-event-id');
const alreadyProcessed = await redis.get(`webhook:processed:${eventId}`);
if (alreadyProcessed) {
return new Response('Already processed', { status: 200 });
}
// Enqueue para processamento assíncrono (retornar 200 rápido)
await queue.add('webhook', { eventId, type: payload.type, payload });
await redis.setex(`webhook:processed:${eventId}`, 86400, '1');
return new Response('OK', { status: 200 });
}
```
3RECOMMEND — Worker assíncrono
```typescript
// webhook-worker.ts
queue.process('webhook', async (job) => {
const { eventId, type, payload } = job.data;
try {
await processWebhookEvent(type, payload);
} catch (err) {
// BullMQ retenta automaticamente com exponential backoff
throw err; // job.attemptsMade controla o número de retentativas
}
});
```
```typescript
it('rejects invalid signature', async () => {
const res = await POST(mockRequest({ signature: 'sha256=invalid' }));
expect(res.status).toBe(401);
});
it('handles duplicate event idempotently', async () => {
await POST(mockRequest({ id: 'evt_123' }));
const res = await POST(mockRequest({ id: 'evt_123' })); // replay
expect(res.status).toBe(200); // não processa de novo, mas retorna 200
});
```
5REFLECT — Documentar e rotacionar secret
Documentar endpoint no README: URL, eventos suportados, como testar localmente
Configurar rotação de secret a cada 90 dias com janela de dual-validation
Reportar telemetria via mcp-skillschain