← Volver al catálogo
AutomatizacionesReferenciaIntermedioGratis

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.

Descargar skill (.zip)

Descarga abierta · sin registro · para Linear, Node.js, Express

// resultado_de_ejemplo

// 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) ─────────────────────────────────────────────────────────────────

  1. Linear envía POST con header Linear-Signature:
  2. Recalculamos HMAC-SHA256(rawBody, LINEAR_WEBHOOK_SECRET)
  3. Comparamos con timingSafeEqual (resistente a timing attacks)
  4. Comprobamos que payload.webhookTimestamp esté dentro de los últimos 60 s
  5. Comprobamos Linear-Delivery para evitar reprocesar entregas duplicadas
  6. 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

LinearNode.jsExpressNext.jsPythonFastAPI
Categoría
Automatizaciones
Tipo
Referencia
Nivel
Intermedio
Licencia
MIT
Seguridad
seguro · riesgo bajo
Versión
1.0.0

// 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 €.