Linear Webhooks: Recepcion y Verificacion
Guia de referencia con codigo listo para usar que muestra como recibir, verificar y gestionar webhooks de Linear en Express, Next.js y FastAPI. Incluye verificacion de firma HMAC-SHA256, proteccion anti-replay y manejo de todos los tipos de eventos de Linear.
Descarga abierta · sin registro · para Linear, Node.js, Express
// Generated with: linear-webhooks skill // https://github.com/hookdeck/webhook-skills // Cliente: TaskFlow SaaS — handler de producción para Linear Webhooks // Stack: Node.js 20 + Express 5
'use strict';
const crypto = require('crypto'); const express = require('express');
const app = express();
// ───────────────────────────────────────────── // 1. VERIFICACIÓN DE FIRMA (HMAC-SHA256) // Linear firma el raw body y lo envía en Linear-Signature (hex). // NO usar express.json() aquí — se debe leer el body RAW. // ───────────────────────────────────────────── function verifyLinearWebhook(rawBody, signatureHeader, secret) { if (!signatureHeader || !secret) return false;
const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex');
try { return crypto.timingSafeEqual( Buffer.from(signatureHeader, 'hex'), Buffer.from(expected, 'hex') ); } catch { return false; // longitudes distintas → no coincide } }
// ───────────────────────────────────────────── // 2. PROTECCIÓN ANTI-REPLAY (ventana 60 s) // Linear incluye webhookTimestamp (epoch ms) en el payload. // Rechazamos entregas con más de 60 segundos de antigüedad. // ───────────────────────────────────────────── function isFreshTimestamp(webhookTimestampMs) { if (typeof webhookTimestampMs !== 'number') return false; return Math.abs(Date.now() - webhookTimestampMs) <= 60_000; }
// ───────────────────────────────────────────── // 3. DEDUPLICACIÓN con Linear-Delivery (UUID v4) // Guardamos las entregas ya procesadas en memoria (en producción // usar Redis o una tabla de BD con TTL de ~24 h). // ───────────────────────────────────────────── const processedDeliveries = new Set();
function isAlreadyProcessed(deliveryId) { if (processedDeliveries.has(deliveryId)) return true; processedDeliveries.add(deliveryId); return false; }
// ───────────────────────────────────────────── // 4. HANDLERS POR TIPO DE EVENTO // ─────────────────────────────────────────────
// Sincroniza cambio de estado de issue a la BD de TaskFlow
async function handleIssueEvent(action, data, delivery) {
if (action === 'create') {
console.log([TaskFlow] Nueva issue en Linear → sincronizando: "${data.title}" [${data.identifier}]);
// await db.issues.insert({ linearId: data.id, title: data.title, state: data.state?.name });
} else if (action === 'update') {
console.log([TaskFlow] Issue actualizada: "${data.title}" — nuevo estado: "${data.state?.name}");
// await db.issues.update({ linearId: data.id }, { state: data.state?.name, updatedAt: new Date() });
} else if (action === 'remove') {
console.log([TaskFlow] Issue eliminada en Linear: ${data.identifier});
// await db.issues.softDelete({ linearId: data.id });
}
}
// Notifica a Slack cuando SLA está en riesgo o roto
async function handleIssueSLAEvent(action, issueData) {
const issue = issueData?.title ?? issueData?.id ?? 'desconocida';
if (action === 'highRisk') {
console.log([TaskFlow] ⚠️ SLA en RIESGO para issue: "${issue}" — enviando alerta a Slack);
// await slack.postMessage({ channel: '#alertas-sla', text: SLA en riesgo: ${issue} });
} else if (action === 'breached') {
console.log([TaskFlow] 🚨 SLA ROTO para issue: "${issue}" — escalando a Project Manager);
// await slack.postMessage({ channel: '#alertas-sla', text: SLA roto: ${issue} @pm-team });
// await email.send({ to: 'pm@taskflow.app', subject: SLA roto: ${issue} });
} else if (action === 'set') {
console.log([TaskFlow] SLA configurado para issue: "${issue}");
}
}
async function handleCommentEvent(action, data) {
console.log([TaskFlow] Comentario ${action} en issue ${data?.issueId}: "${data?.body?.slice(0, 80)}...");
}
// ───────────────────────────────────────────── // 5. ENDPOINT PRINCIPAL POST /webhooks/linear // ───────────────────────────────────────────── app.post( '/webhooks/linear', express.raw({ type: 'application/json' }), // <-- RAW body obligatorio para HMAC async (req, res) => { const signature = req.headers['linear-signature']; const event = req.headers['linear-event']; // 'Issue' | 'Comment' | 'IssueSLA' … const delivery = req.headers['linear-delivery']; // UUID v4 para idempotencia const secret = process.env.LINEAR_WEBHOOK_SECRET;
// ── A. Verificar firma ──────────────────────────────────────────────
if (!verifyLinearWebhook(req.body, signature, secret)) {
console.warn(`[TaskFlow] Firma inválida — delivery: ${delivery}`);
return res.status(400).json({ error: 'Invalid signature' });
}
// ── B. Parsear payload ──────────────────────────────────────────────
let payload;
try {
payload = JSON.parse(req.body.toString('utf-8'));
} catch {
return res.status(400).json({ error: 'Invalid JSON body' });
}
// ── C. Protección anti-replay ───────────────────────────────────────
if (!isFreshTimestamp(payload.webhookTimestamp)) {
console.warn(`[TaskFlow] Entrega antigua rechazada — delivery: ${delivery}, ts: ${payload.webhookTimestamp}`);
return res.status(400).json({ error: 'Stale webhook' });
}
// ── D. Deduplicación ────────────────────────────────────────────────
if (isAlreadyProcessed(delivery)) {
console.log(`[TaskFlow] Entrega duplicada ignorada: ${delivery}`);
return res.status(200).json({ status: 'already_processed' });
}
// ── E. Enrutamiento por tipo de evento ──────────────────────────────
console.log(`[TaskFlow] Evento Linear recibido: ${event} / ${payload.action} (delivery: ${delivery})`);
try {
switch (event) {
case 'Issue':
await handleIssueEvent(payload.action, payload.data, delivery);
break;
case 'Comment':
await handleCommentEvent(payload.action, payload.data);
break;
case 'IssueSLA':
await handleIssueSLAEvent(payload.action, payload.issueData);
break;
case 'Project':
console.log(`[TaskFlow] Proyecto ${payload.action}: "${payload.data?.name}"`);
break;
default:
console.log(`[TaskFlow] Evento no manejado: ${event}`);
}
return res.status(200).json({ status: 'ok', event, action: payload.action });
} catch (err) {
console.error(`[TaskFlow] Error procesando evento ${event}:`, err.message);
// Devolvemos 500 para que Linear reintente la entrega
return res.status(500).json({ error: 'Handler error' });
}
} );
// ─────────────────────────────────────────────
// 6. ARRANQUE DEL SERVIDOR
// ─────────────────────────────────────────────
const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => {
console.log([TaskFlow] Servidor webhook escuchando en http://localhost:${PORT}/webhooks/linear);
console.log([TaskFlow] Tunelizar con: npx hookdeck-cli listen ${PORT} linear --path /webhooks/linear);
});
module.exports = app; // para tests con supertest
/* ───────────────────────────────────────────────────────────────── VARIABLES DE ENTORNO necesarias ───────────────────────────────────────────────────────────────── LINEAR_WEBHOOK_SECRET=xxx # visible una sola vez al crear el webhook en Linear API Settings PORT=3000 # (opcional) puerto del servidor
CÓMO PROBAR LOCALMENTE ───────────────────────────────────────────────────────────────── npx hookdeck-cli listen 3000 linear --path /webhooks/linear
Usar la URL de Hookdeck como Webhook URL en Linear > Settings > API > Webhooks
FLUJO DE VERIFICACIÓN (resumen) ─────────────────────────────────────────────────────────────────
- Linear envía POST con header Linear-Signature:
- Recalculamos HMAC-SHA256(rawBody, LINEAR_WEBHOOK_SECRET)
- Comparamos con timingSafeEqual (resistente a timing attacks)
- Comprobamos que payload.webhookTimestamp esté dentro de los últimos 60 s
- Comprobamos Linear-Delivery para evitar reprocesar entregas duplicadas
- Enrutamos por Linear-Event a su handler específico ───────────────────────────────────────────────────────────────── */
// qué_hace
Proporciona codigo de verificacion de webhooks de Linear con HMAC-SHA256 y ejemplos de handlers para Express y FastAPI.
// cómo_lo_hace
Documenta la validacion de la cabecera Linear-Signature, la proteccion anti-replay por timestamp y el enrutamiento por tipo de evento con snippets listos para produccion.
// ejemplo_de_uso
Úsala cuando construyes integraciones con Linear y necesitas reaccionar a cambios de issues en tiempo real de forma segura. Ej.: recibes un webhook de Linear al cambiar el estado de una tarea y verificas la firma HMAC antes de procesarlo.
// plataformas
// opiniones_de_la_comunidad
Opiniones
Cargando opiniones…
// pase_cultiva_ia
Llévate todo el arsenal con el Pase
Todas las skills, prompts y automatizaciones del catálogo en un único archivo, listas para usar: un pago, acceso de por vida y las novedades que añadamos. Sin suscripción.
Pago único · IVA incluido · pago seguro con Stripe.
Acceso inmediato · si no es lo que esperabas, te devolvemos los 10 €.