{
  "name": "WAIA Connect — Webhook (Header Auth + verificación de firma opcional)",
  "nodes": [
    {
      "parameters": {
        "content": "## WAIA Connect — leeme\n\n**Camino recomendado (sin código): Autenticación por header.**\n1. Abrí el nodo **Webhook (POST)** → *Authentication: Header Auth* (ya viene configurado).\n2. Creá una credencial **Header Auth**: *Name* = el header que Connect manda (por defecto `X-Connect-Token`), *Value* = el token que te dio Connect (`wct_…`).\n\nEso es todo. n8n valida el header antes de correr el flujo.\n\n**Opcional — verificación de firma (integridad + anti-replay):** pegá tu `whsec_…` en el nodo **Config**. Si dejás el placeholder, el nodo *Verificar firma* no hace nada (podés desactivarlo).\n\n⚠ **No exportes ni compartas este workflow con el secret adentro** — el nodo Config lo guarda EN el workflow. El token del header vive en la credencial de n8n, no acá.",
        "height": 340,
        "width": 460
      },
      "id": "node-sticky",
      "name": "WAIA Connect — leeme",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [-560, 120]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "waia-connect",
        "authentication": "headerAuth",
        "responseMode": "onReceived",
        "options": {
          "rawBody": true
        }
      },
      "id": "node-webhook",
      "name": "Webhook (POST)",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1.1,
      "position": [-40, 300],
      "webhookId": "waia-connect-webhook",
      "notes": "POST (n8n los crea en GET por defecto). Authentication = Header Auth: creá una credencial con Name=X-Connect-Token y Value=tu token wct_. Raw Body activado (para la firma opcional)."
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "assign-secret",
              "name": "secret",
              "type": "string",
              "value": "whsec_REEMPLAZAR_POR_EL_TUYO"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "node-config",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [220, 300],
      "notes": "Pegá acá tu whsec_ SOLO si querés verificación de firma. Funciona en cualquier n8n, sin variables de entorno ni licencia. Dejá el placeholder si validás por header."
    },
    {
      "parameters": {
        "language": "javaScript",
        "jsCode": "// OPTIONAL — HMAC signature verification (X-Connect-Signature-256).\n// The recommended path is the Webhook node's native Header Auth (two clicks, no code).\n// This node ADDS end-to-end signature verification (body integrity + anti-replay). It\n// reads the secret from the 'Config' Set node — NOT from an environment variable (n8n 2.0\n// blocks env access in Code nodes by default, and Variables is a licensed feature).\n//\n// If you keep the placeholder secret in 'Config', this node does nothing (it passes the\n// data through). You can also disable it entirely if you only use Header Auth.\nconst crypto = require('crypto');\nconst secret = $('Config').first().json.secret;\n\n// Placeholder secret → verification off. Pass the item through unchanged.\nif (!secret || secret === 'whsec_REEMPLAZAR_POR_EL_TUYO') {\n  return $input.all();\n}\n\n// Read the RAW body + headers from the Webhook node (Raw Body is enabled there). The\n// signature is over the exact bytes Connect sent — re-serializing the JSON breaks it.\nconst hook = $('Webhook (POST)').first();\nconst headers = hook.json.headers || {};\nconst sig = headers['x-connect-signature-256'];   // 'sha256=<hex>'\nconst ts  = headers['x-connect-timestamp'];        // unix seconds, signed inside the HMAC\nif (!sig || !ts) {\n  throw new Error('Missing signature headers (X-Connect-Signature-256 / X-Connect-Timestamp).');\n}\n\n// Anti-replay: reject deliveries older than 5 minutes. The timestamp is inside the HMAC,\n// so a captured payload cannot be replayed later.\nconst skew = Math.abs(Math.floor(Date.now() / 1000) - Number(ts));\nif (!Number.isFinite(Number(ts)) || skew > 300) {\n  throw new Error('Timestamp outside the 5-minute window (replay?): skew=' + skew + 's');\n}\n\nlet rawBody;\nif (hook.binary && hook.binary.data && hook.binary.data.data) {\n  rawBody = Buffer.from(hook.binary.data.data, 'base64').toString('utf8');\n} else {\n  rawBody = typeof hook.json.body === 'string' ? hook.json.body : JSON.stringify(hook.json.body);\n}\n\n// HMAC-SHA256 of `${timestamp}.${rawBody}`, compared in CONSTANT time.\nconst expected = 'sha256=' + crypto.createHmac('sha256', secret).update(ts + '.' + rawBody).digest('hex');\nconst a = Buffer.from(sig);\nconst b = Buffer.from(expected);\nif (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {\n  throw new Error('Invalid signature — dropping the delivery.');\n}\n\n// Signature OK. The envelope stays at item.json.body for the Switch and the branches.\nreturn $input.all();"
      },
      "id": "node-verify",
      "name": "Verificar firma (opcional)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [480, 300],
      "notes": "OPCIONAL. Si ya validás por Header Auth, podés desactivar este nodo. Lee el secret del nodo Config, nunca de una variable de entorno."
    },
    {
      "parameters": {
        "rules": {
          "values": [
            { "conditions": { "options": { "caseSensitive": true, "typeValidation": "strict" }, "conditions": [ { "id": "c-message.received", "leftValue": "={{ $json.body.type }}", "rightValue": "message.received", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "message.received" },
            { "conditions": { "options": { "caseSensitive": true, "typeValidation": "strict" }, "conditions": [ { "id": "c-message.echo", "leftValue": "={{ $json.body.type }}", "rightValue": "message.echo", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "message.echo" },
            { "conditions": { "options": { "caseSensitive": true, "typeValidation": "strict" }, "conditions": [ { "id": "c-message.status", "leftValue": "={{ $json.body.type }}", "rightValue": "message.status", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "message.status" },
            { "conditions": { "options": { "caseSensitive": true, "typeValidation": "strict" }, "conditions": [ { "id": "c-connection.created", "leftValue": "={{ $json.body.type }}", "rightValue": "connection.created", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "connection.created" },
            { "conditions": { "options": { "caseSensitive": true, "typeValidation": "strict" }, "conditions": [ { "id": "c-connection.status_changed", "leftValue": "={{ $json.body.type }}", "rightValue": "connection.status_changed", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "connection.status_changed" },
            { "conditions": { "options": { "caseSensitive": true, "typeValidation": "strict" }, "conditions": [ { "id": "c-connection.usage_threshold_reached", "leftValue": "={{ $json.body.type }}", "rightValue": "connection.usage_threshold_reached", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "connection.usage_threshold_reached" },
            { "conditions": { "options": { "caseSensitive": true, "typeValidation": "strict" }, "conditions": [ { "id": "c-usage.threshold_reached", "leftValue": "={{ $json.body.type }}", "rightValue": "usage.threshold_reached", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "usage.threshold_reached" },
            { "conditions": { "options": { "caseSensitive": true, "typeValidation": "strict" }, "conditions": [ { "id": "c-history.synced", "leftValue": "={{ $json.body.type }}", "rightValue": "history.synced", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "history.synced" },
            { "conditions": { "options": { "caseSensitive": true, "typeValidation": "strict" }, "conditions": [ { "id": "c-webhook.test", "leftValue": "={{ $json.body.type }}", "rightValue": "webhook.test", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "webhook.test" }
          ]
        },
        "options": { "fallbackOutput": "extra" }
      },
      "id": "node-switch",
      "name": "Ramificar por type",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3,
      "position": [740, 300],
      "notes": "El tipo de evento viaja en $json.body.type (el sobre entero está en $json.body)."
    },
    { "parameters": {}, "id": "noop-0", "name": "message.received", "type": "n8n-nodes-base.noOp", "typeVersion": 1, "position": [1000, -225], "notes": "Un cliente te escribió. $json.body.data.message = mensaje de WhatsApp; data.contacts[]. Mapealo a tu CRM / dispará tu bot.", "notesInFlow": true },
    { "parameters": {}, "id": "noop-1", "name": "message.echo", "type": "n8n-nodes-base.noOp", "typeVersion": 1, "position": [1000, -75], "notes": "🌟 Coexistence: un mensaje que el DUEÑO envió DESDE EL TELÉFONO, con contenido. data.origin='device' (no consume plan). Sincronizá tu CRM con lo que responde tu equipo desde el celular.", "notesInFlow": true },
    { "parameters": {}, "id": "noop-2", "name": "message.status", "type": "n8n-nodes-base.noOp", "typeVersion": 1, "position": [1000, 75], "notes": "Acuse de un mensaje que enviaste por /v1. data.status=sent|delivered|read|failed, data.origin (api|device). Actualizá el estado en tu sistema.", "notesInFlow": true },
    { "parameters": {}, "id": "noop-3", "name": "connection.created", "type": "n8n-nodes-base.noOp", "typeVersion": 1, "position": [1000, 225], "notes": "Se conectó un número nuevo. Trae tu externalId en el sobre. Provisioná al cliente final en tu sistema.", "notesInFlow": true },
    { "parameters": {}, "id": "noop-4", "name": "connection.status_changed", "type": "n8n-nodes-base.noOp", "typeVersion": 1, "position": [1000, 375], "notes": "Cambió el estado de una conexión (ej. data.status='disconnected'). Alertá / pausá envíos a ese número.", "notesInFlow": true },
    { "parameters": {}, "id": "noop-5", "name": "connection.usage_threshold_reached", "type": "n8n-nodes-base.noOp", "typeVersion": 1, "position": [1000, 525], "notes": "El límite de UN número llegó a un umbral (70/90/100%). Avisale a ese cliente final o subile el tope.", "notesInFlow": true },
    { "parameters": {}, "id": "noop-6", "name": "usage.threshold_reached", "type": "n8n-nodes-base.noOp", "typeVersion": 1, "position": [1000, 675], "notes": "El plan de la CUENTA llegó a un umbral. connection viene en null. Decidí upgrade / corte.", "notesInFlow": true },
    { "parameters": {}, "id": "noop-7", "name": "history.synced", "type": "n8n-nodes-base.noOp", "typeVersion": 1, "position": [1000, 825], "notes": "(Coexistence) resumen del backfill al conectar: data.conversations, data.messages, data.shared. Los mensajes históricos NO se reenvían uno por uno.", "notesInFlow": true },
    { "parameters": {}, "id": "noop-8", "name": "webhook.test", "type": "n8n-nodes-base.noOp", "typeVersion": 1, "position": [1000, 975], "notes": "El evento del botón 'Probar' del panel. connection=null, data.test=true. Usalo para validar tu endpoint; no lo trates como tráfico real.", "notesInFlow": true },
    { "parameters": {}, "id": "noop-9", "name": "(no manejado)", "type": "n8n-nodes-base.noOp", "typeVersion": 1, "position": [1000, 1125], "notes": "Un type que este workflow no ramifica. Logueá y respondé 2xx igual — Connect puede sumar tipos nuevos; no rompas por uno desconocido.", "notesInFlow": true }
  ],
  "connections": {
    "Webhook (POST)": { "main": [ [ { "node": "Config", "type": "main", "index": 0 } ] ] },
    "Config": { "main": [ [ { "node": "Verificar firma (opcional)", "type": "main", "index": 0 } ] ] },
    "Verificar firma (opcional)": { "main": [ [ { "node": "Ramificar por type", "type": "main", "index": 0 } ] ] },
    "Ramificar por type": {
      "main": [
        [ { "node": "message.received", "type": "main", "index": 0 } ],
        [ { "node": "message.echo", "type": "main", "index": 0 } ],
        [ { "node": "message.status", "type": "main", "index": 0 } ],
        [ { "node": "connection.created", "type": "main", "index": 0 } ],
        [ { "node": "connection.status_changed", "type": "main", "index": 0 } ],
        [ { "node": "connection.usage_threshold_reached", "type": "main", "index": 0 } ],
        [ { "node": "usage.threshold_reached", "type": "main", "index": 0 } ],
        [ { "node": "history.synced", "type": "main", "index": 0 } ],
        [ { "node": "webhook.test", "type": "main", "index": 0 } ],
        [ { "node": "(no manejado)", "type": "main", "index": 0 } ]
      ]
    }
  },
  "active": false,
  "settings": { "executionOrder": "v1" },
  "pinData": {},
  "meta": { "templateCredsSetupCompleted": false }
}
