{
  "name": "Needle leads (LeadEvent v1)",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "needle-leads",
        "responseMode": "onReceived",
        "options": {
          "rawBody": true
        }
      },
      "id": "0f6f8a52-3c1e-4d0b-9a51-6a1f0c2b7e01",
      "name": "Needle webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [0, 0],
      "webhookId": "needle-leads"
    },
    {
      "parameters": {
        "jsCode": "// Verifies Needle's X-Needle-Signature on the raw request body, then passes\n// lead events on. Self-hosted n8n: set NODE_FUNCTION_ALLOW_BUILTIN=crypto\n// so this node may require('crypto').\nconst crypto = require('crypto');\n\n// Paste the signing secret Needle showed after \"Test and enable\" (the whole\n// value, including the whsec_ prefix). Self-hosted instances that allow env\n// access in Code nodes can set NEEDLE_SIGNING_SECRET instead.\nlet SECRET = 'whsec_REPLACE_ME';\ntry {\n  if ($env.NEEDLE_SIGNING_SECRET) SECRET = $env.NEEDLE_SIGNING_SECRET;\n} catch (err) {\n  // Env access is blocked on this instance; keep the pasted value.\n}\nconst TOLERANCE_SECONDS = 5 * 60;\n\nconst out = [];\nconst items = $input.all();\nfor (let i = 0; i < items.length; i++) {\n  const headers = items[i].json.headers || {};\n  const header = String(headers['x-needle-signature'] || '');\n\n  // The Webhook node's \"Raw Body\" option keeps the exact bytes Needle signed.\n  const raw = (await this.helpers.getBinaryDataBuffer(i, 'data')).toString(\n    'utf8'\n  );\n\n  const parts = {};\n  for (const kv of header.split(',')) {\n    const at = kv.indexOf('=');\n    if (at > 0) parts[kv.slice(0, at).trim()] = kv.slice(at + 1).trim();\n  }\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || !parts.v1) {\n    throw new Error('Missing or malformed X-Needle-Signature header');\n  }\n  if (Math.abs(Math.floor(Date.now() / 1000) - t) > TOLERANCE_SECONDS) {\n    throw new Error('Needle signature timestamp is older than 5 minutes');\n  }\n  const expected = Buffer.from(\n    crypto.createHmac('sha256', SECRET).update(`${t}.${raw}`).digest('hex')\n  );\n  const given = Buffer.from(parts.v1);\n  if (\n    expected.length !== given.length ||\n    !crypto.timingSafeEqual(expected, given)\n  ) {\n    throw new Error('Invalid Needle signature');\n  }\n\n  const event = JSON.parse(raw);\n  // \"Test and enable\" sends event \"test\": verified above, nothing to store.\n  if (event.event !== 'lead.created') continue;\n  out.push({ json: event });\n}\nreturn out;\n"
      },
      "id": "0f6f8a52-3c1e-4d0b-9a51-6a1f0c2b7e02",
      "name": "Verify signature",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [240, 0]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "6c7c4f0e-2a8e-4b0e-9a3f-1d2e3f4a5b61",
              "leftValue": "={{ $json.lead.tier }}",
              "rightValue": "hot_lead",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "0f6f8a52-3c1e-4d0b-9a51-6a1f0c2b7e03",
      "name": "Hot lead?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [480, 0]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#leads"
        },
        "text": "=Hot lead on {{ $json.lead.platform }}: {{ $json.lead.title }}\n{{ $json.lead.url }}\nOpen in Needle: {{ $json.lead.dashboard_url }}",
        "otherOptions": {}
      },
      "id": "0f6f8a52-3c1e-4d0b-9a51-6a1f0c2b7e04",
      "name": "Slack: hot lead (set channel + credential)",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [720, -100]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": ""
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Needle leads"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "occurred_at": "={{ $json.occurred_at }}",
            "brand_name": "={{ $json.brand.name }}",
            "lead_tier": "={{ $json.lead.tier }}",
            "lead_platform": "={{ $json.lead.platform }}",
            "lead_title": "={{ $json.lead.title }}",
            "lead_url": "={{ $json.lead.url }}",
            "lead_author": "={{ $json.lead.author }}",
            "lead_excerpt": "={{ $json.lead.excerpt }}",
            "lead_score": "={{ $json.lead.lead_score }}",
            "lead_matched_term": "={{ $json.lead.matched_term }}",
            "lead_competitor_mentions": "={{ ($json.lead.competitor_mentions || []).join(', ') }}",
            "lead_dashboard_url": "={{ $json.lead.dashboard_url }}",
            "event_id": "={{ $json.event_id }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "occurred_at",
              "displayName": "occurred_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "brand_name",
              "displayName": "brand_name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "lead_tier",
              "displayName": "lead_tier",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "lead_platform",
              "displayName": "lead_platform",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "lead_title",
              "displayName": "lead_title",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "lead_url",
              "displayName": "lead_url",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "lead_author",
              "displayName": "lead_author",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "lead_excerpt",
              "displayName": "lead_excerpt",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "lead_score",
              "displayName": "lead_score",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "lead_matched_term",
              "displayName": "lead_matched_term",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "lead_competitor_mentions",
              "displayName": "lead_competitor_mentions",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "lead_dashboard_url",
              "displayName": "lead_dashboard_url",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "event_id",
              "displayName": "event_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      },
      "id": "0f6f8a52-3c1e-4d0b-9a51-6a1f0c2b7e05",
      "name": "Sheets: log every lead (set sheet + credential)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [720, 120]
    }
  ],
  "connections": {
    "Needle webhook": {
      "main": [
        [
          {
            "node": "Verify signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify signature": {
      "main": [
        [
          {
            "node": "Hot lead?",
            "type": "main",
            "index": 0
          },
          {
            "node": "Sheets: log every lead (set sheet + credential)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hot lead?": {
      "main": [
        [
          {
            "node": "Slack: hot lead (set channel + credential)",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "active": false,
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
