{
  "name": "Enroutia · Alias watch to Slack and email",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "enroutia-alias-watch",
        "responseMode": "responseNode",
        "options": {
          "rawBody": true
        }
      },
      "name": "Enroutia events",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        0,
        0
      ],
      "webhookId": "enroutia-alias-watch"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "enroutia_webhook_secret",
              "name": "ENROUTIA_WEBHOOK_SECRET",
              "value": "REPLACE_WITH_YOUR_WEBHOOK_SECRET",
              "type": "string"
            },
            {
              "id": "enroutia_api",
              "name": "ENROUTIA_API",
              "value": "https://platform.enroutia.com",
              "type": "string"
            },
            {
              "id": "alert_email_from",
              "name": "ALERT_EMAIL_FROM",
              "value": "alerts@example.com",
              "type": "string"
            },
            {
              "id": "alert_email_to",
              "name": "ALERT_EMAIL_TO",
              "value": "you@example.com",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {
          "stripBinary": false
        }
      },
      "name": "Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Verifies X-Webhook-Signature (t=<unix>,v1=<hex>): HMAC-SHA256 of the secret over\n// \"{t}.{raw body}\", five minutes of tolerance, constant-time comparison.\n// Needs NODE_FUNCTION_ALLOW_BUILTIN=crypto on the n8n instance (see the README).\nconst crypto = require('crypto');\nconst TOLERANCE_S = 300;\n\n// The delivery as the Webhook node received it, and the secret from the Settings node\n// (n8n 2.x blocks environment access in nodes by default, so nothing here reads the environment).\nconst item = $input.first();\nconst received = $('Enroutia events').first();\nconst headers = received.json.headers || item.json.headers || {};\nconst secret = String($('Settings').first().json.ENROUTIA_WEBHOOK_SECRET || '');\n\n// The signed body is the one that arrived, byte for byte: re-serialising the parsed JSON\n// changes spacing and key order and the signature stops matching. That is why the\n// Webhook node has \"Raw Body\" on and the body is read from the binary.\nlet raw = null;\ntry {\n  raw = await this.helpers.getBinaryDataBuffer(0, 'data');\n} catch (error) {\n  const binary = item.binary || received.binary || {};\n  const inline = binary.data && binary.data.data;\n  if (inline) raw = Buffer.from(inline, 'base64');\n}\n\nconst reject = (reason) => [{ json: { valid: false, reason } }];\nif (!secret || secret === 'REPLACE_WITH_YOUR_WEBHOOK_SECRET') return reject('secret_missing');\nif (!raw) return reject('raw_body_unavailable');\n\nconst header = String(headers['x-webhook-signature'] || '');\nconst parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));\nconst t = Number(parts.t);\nif (!Number.isFinite(t)) return reject('signature_malformed');\nif (Math.abs(Date.now() / 1000 - t) > TOLERANCE_S) return reject('signature_expired');\n\nconst expected = crypto.createHmac('sha256', secret).update(t + '.').update(raw).digest();\nconst given = Buffer.from(String(parts.v1 || ''), 'hex');\nif (given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) {\n  return reject('signature_mismatch');\n}\n\nlet payload;\ntry {\n  payload = JSON.parse(raw.toString('utf8'));\n} catch (error) {\n  return reject('body_not_json');\n}\n\nreturn [{\n  json: {\n    valid: true,\n    event: String(headers['x-webhook-event'] || payload.event || ''),\n    data: payload.data || {},\n  },\n}];\n"
      },
      "name": "Verify signature",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "signature-valid",
              "leftValue": "={{ $json.valid }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "Signature valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        660,
        0
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\"ok\":true}",
        "options": {
          "responseCode": 200
        }
      },
      "name": "Accept (200)",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        880,
        -120
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\"ok\":false}",
        "options": {
          "responseCode": 401
        }
      },
      "name": "Reject (401)",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        880,
        160
      ]
    },
    {
      "parameters": {
        "jsCode": "// One message per alias event; every other event is dropped here.\nconst { event, data } = $('Verify signature').item.json;\nconst panel = $('Settings').first().json.ENROUTIA_API + '/panel/aliases';\nconst alias = 'enroutia/' + (data.alias || '?');\n\nif (event === 'alias_check_degraded') {\n  const rollback = data.rollback_to_version\n    ? `Restore version ${data.rollback_to_version} (${data.rollback_to_model}) from the panel.`\n    : 'There is no earlier version to restore.';\n  return [{ json: {\n    event,\n    subject: `${alias}: checks at ${data.pass_pct} % (threshold ${data.threshold_pct} %)`,\n    text: [\n      `The checks on ${alias} passed ${data.pass_pct} % of ${data.total} calls in the last ${data.window_hours} h, below the ${data.threshold_pct} % threshold.`,\n      `It points at ${data.current_model} (version ${data.current_version}).`,\n      rollback,\n      panel,\n    ].join('\\n'),\n  } }];\n}\n\nif (event === 'alias_proposal') {\n  const cost = data.cost_delta_pct <= 0 ? `${-data.cost_delta_pct} % cheaper` : `${data.cost_delta_pct} % dearer`;\n  return [{ json: {\n    event,\n    subject: `${alias}: ${data.candidate_model} matches ${data.quality_pct} % and is ${cost}`,\n    text: [\n      `The alias watch replayed ${data.examples} saved examples of ${alias} on ${data.candidate_model} (today: ${data.current_model}).`,\n      `Quality ${data.quality_pct} %, cost ${cost}. The test cost ${data.billed_cents} cents.`,\n      `Nothing changes until you accept proposal ${data.proposal_id} in the panel.`,\n      panel,\n    ].join('\\n'),\n  } }];\n}\n\nreturn [];\n"
      },
      "name": "Compose message",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        -120
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "#enroutia-alerts",
          "mode": "name"
        },
        "text": "=*{{ $json.subject }}*\n{{ $json.text }}",
        "otherOptions": {}
      },
      "name": "Post to Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1320,
        -220
      ],
      "credentials": {
        "slackApi": {
          "name": "Slack account"
        }
      }
    },
    {
      "parameters": {
        "fromEmail": "={{ $('Settings').first().json.ALERT_EMAIL_FROM }}",
        "toEmail": "={{ $('Settings').first().json.ALERT_EMAIL_TO }}",
        "subject": "={{ $json.subject }}",
        "emailFormat": "text",
        "text": "={{ $json.text }}",
        "options": {}
      },
      "name": "Send email",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1320,
        -20
      ],
      "credentials": {
        "smtp": {
          "name": "SMTP account"
        }
      }
    }
  ],
  "connections": {
    "Enroutia events": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify signature": {
      "main": [
        [
          {
            "node": "Signature valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Signature valid?": {
      "main": [
        [
          {
            "node": "Accept (200)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reject (401)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Accept (200)": {
      "main": [
        [
          {
            "node": "Compose message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compose message": {
      "main": [
        [
          {
            "node": "Post to Slack",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings": {
      "main": [
        [
          {
            "node": "Verify signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
