{
  "id": "4a975eda11394da6846d9c132b176d14",
  "name": "Manin Studio — подписанный webhook с дедупликацией",
  "nodes": [
    {
      "id": "832cfbbb-9167-4899-958e-920f8416d762",
      "name": "Принять подписанное событие",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        0,
        300
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "manin-inbox-change-me",
        "responseMode": "responseNode",
        "options": {
          "rawBody": true
        }
      },
      "webhookId": "5a984c5e-700d-4a0b-88b5-606fe83bbf35"
    },
    {
      "id": "c33256c0-17f8-4ec1-afd8-4dbb3ddbcacf",
      "name": "HMAC SHA-256 исходных байтов",
      "type": "n8n-nodes-base.crypto",
      "typeVersion": 2,
      "position": [
        240,
        300
      ],
      "parameters": {
        "action": "hmac",
        "binaryData": true,
        "binaryPropertyName": "data",
        "type": "SHA256",
        "encoding": "hex",
        "dataPropertyName": "computedSignature"
      }
    },
    {
      "id": "b67abbfd-8fa4-409c-9361-3e6f6ba4172f",
      "name": "Проверить подпись и структуру",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        300
      ],
      "parameters": {
        "jsCode": "const request = $input.first().json;\nconst supplied = String(request.headers?.['x-signature-256'] || '').replace(/^sha256=/i, '').toLowerCase();\nconst computed = String(request.computedSignature || '').toLowerCase();\nlet mismatch = 0;\nif (!/^[a-f0-9]{64}$/.test(supplied) || !/^[a-f0-9]{64}$/.test(computed)) mismatch = 1;\nfor (let i = 0; i < 64; i++) mismatch |= (supplied.charCodeAt(i) || 0) ^ (computed.charCodeAt(i) || 0);\nconst body = request.body;\nconst validBody = !!body && !Array.isArray(body) && typeof body === 'object' && /^[A-Za-z0-9:_-]{1,100}$/.test(String(body.id || '')) && /^[A-Za-z0-9._-]{1,80}$/.test(String(body.type || ''));\nconst type = validBody ? String(body.type) : '';\nreturn [{json: {signatureOk: mismatch === 0, validBody, eventId: validBody ? String(body.id) : '', eventType: type, route: type === 'lead.created' ? 'leads' : type === 'order.created' ? 'orders' : 'other'}}];"
      }
    },
    {
      "id": "6edfd21c-3c71-45d8-b6c4-6246dbd3203b",
      "name": "Подпись верна?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        720,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "36a9326f-2284-4282-89e8-997f2379215e",
              "leftValue": "={{ $json.signatureOk }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "d869274d-e705-4043-a422-46de1382ad3b",
      "name": "Данные корректны?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        960,
        220
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "467ca434-21e8-4efe-b923-8d23ba647324",
              "leftValue": "={{ $json.validBody }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "b4a1d11d-6ec7-475e-a1d8-d7a7023fc0f6",
      "name": "Атомарно записать и направить",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1200,
        140
      ],
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH saved AS (INSERT INTO ms_webhook_inbox(event_id,event_type,route) VALUES ($1::text,$2::text,$3::text) ON CONFLICT ON CONSTRAINT ms_webhook_inbox_pkey DO NOTHING RETURNING 1) SELECT EXISTS(SELECT 1 FROM saved) AS accepted;",
        "options": {
          "queryReplacement": "={{ [$json.eventId, $json.eventType, $json.route] }}"
        }
      }
    },
    {
      "id": "65d5554a-c3a2-46a4-bfdf-63c1755b76c5",
      "name": "Ответ 202 или 200",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        1440,
        140
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ {accepted: $json.accepted, duplicate: !$json.accepted, route: $('Проверить подпись и структуру').item.json.route} }}",
        "options": {
          "responseCode": "={{ $json.accepted ? 202 : 200 }}"
        }
      }
    },
    {
      "id": "323989f5-40ad-4479-97cc-a1883c0b694a",
      "name": "Ответ 400",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        1200,
        300
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\"error\":\"invalid event\"}",
        "options": {
          "responseCode": 400
        }
      }
    },
    {
      "id": "d19e9c7d-cd95-4119-a430-86b4176c8f48",
      "name": "Ответ 401",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        960,
        430
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\"error\":\"invalid signature\"}",
        "options": {
          "responseCode": 401
        }
      }
    }
  ],
  "connections": {
    "Принять подписанное событие": {
      "main": [
        [
          {
            "node": "HMAC SHA-256 исходных байтов",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HMAC SHA-256 исходных байтов": {
      "main": [
        [
          {
            "node": "Проверить подпись и структуру",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Проверить подпись и структуру": {
      "main": [
        [
          {
            "node": "Подпись верна?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Подпись верна?": {
      "main": [
        [
          {
            "node": "Данные корректны?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Ответ 401",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Данные корректны?": {
      "main": [
        [
          {
            "node": "Атомарно записать и направить",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Ответ 400",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Атомарно записать и направить": {
      "main": [
        [
          {
            "node": "Ответ 202 или 200",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveDataErrorExecution": "none",
    "saveDataSuccessExecution": "none"
  },
  "active": false,
  "pinData": {}
}
