{
  "name": "FYP Now alerts (verify + format)",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "fypnow",
        "responseMode": "onReceived",
        "options": {
          "rawBody": true
        }
      },
      "name": "FYP Now webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        0
      ],
      "webhookId": "fypnow-alerts"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// FYP Now alerts: verify X-FYPNow-Signature, drop forged or replayed requests,\n// then shape one plain-text `message` per alert. Docs: docs/integrations.md.\n//\n// Set the secret: paste it below (n8n Cloud), or on self-hosted n8n with env access\n// allowed, set FYPNOW_WEBHOOK_SECRET. Self-hosted n8n must allow the crypto module:\n// NODE_FUNCTION_ALLOW_BUILTIN=crypto (also on the task runner in external mode).\nconst crypto = require('crypto');\nlet SECRET = 'PASTE_WHSEC_SECRET_HERE';\ntry { SECRET = $env.FYPNOW_WEBHOOK_SECRET || SECRET; } catch (e) { /* env access blocked */ }\nconst MAX_AGE_SECONDS = 300;\n\nconst compact = (n) => {\n  n = Number(n) || 0;\n  for (const [div, s] of [[1e9, 'B'], [1e6, 'M'], [1e3, 'K']]) {\n    if (Math.abs(n) >= div) return (n / div).toFixed(1).replace(/\\.0$/, '') + s;\n  }\n  return String(n);\n};\nconst mult = (m) => (m == null ? '' : `${Number(m).toFixed(1).replace(/\\.0$/, '')}x`);\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-fypnow-signature'] || '');\n  if (!header) continue;\n  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));\n  // Verify against the RAW body (Webhook node option \"Raw Body\" is on).\n  const raw = (await this.helpers.getBinaryDataBuffer(i, 'data')).toString('utf8');\n  const expected = crypto.createHmac('sha256', SECRET).update(`${parts.t}.${raw}`).digest('hex');\n  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < MAX_AGE_SECONDS;\n  const valid = fresh && typeof parts.v1 === 'string' && parts.v1.length === expected.length &&\n    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));\n  if (!valid) continue;\n\n  const body = JSON.parse(raw);\n  const d = body.data || {};\n  let message;\n  if (body.event === 'breakout.detected') {\n    const who = d.is_own ? 'Your video' : (d.username ? '@' + d.username : 'A tracked creator');\n    message = `${who} broke out: ${mult(d.multiplier)} their usual views ` +\n      `(${compact(d.views)} views) ${d.video_url || ''}`.trim();\n  } else if (body.event === 'niche.refreshed') {\n    const top = (d.top || []).slice(0, 3)\n      .map((v) => `${v.username ? '@' + v.username : 'A creator'} ${compact(v.views)} views${v.multiplier != null ? ' \u00b7 ' + mult(v.multiplier) : ''} ${v.video_url || ''}`.trim())\n      .join('\\n');\n    message = `${d.niche_name || 'Your agent'}: ${d.new_videos} new videos\\n${top}`.trim();\n  } else if (body.event === 'analysis.completed') {\n    const subject = d.source === 'upload' || !d.username ? 'your uploaded draft' : `@${d.username}'s video`;\n    const facts = [d.hook_type && `hook: ${d.hook_type}`, d.content_type && `format: ${d.content_type}`]\n      .filter(Boolean).join(', ');\n    message = [`${d.source === 'reanalysis' ? 'Re-analysis' : 'Analysis'} ready: ${subject}`,\n      facts && `(${facts})`, d.dashboard_url].filter(Boolean).join(' ');\n  } else if (body.event === 'creator.posted') {\n    const who = d.username ? '@' + d.username : 'A tracked creator';\n    message = [`${d.is_own ? 'Your account ' : ''}${who} just posted:`, (d.description || '').slice(0, 120),\n      d.video_url].filter(Boolean).join(' ');\n  } else {\n    message = 'FYP Now is connected. Alerts will arrive here.';\n  }\n  out.push({ json: { id: body.id, event: body.event, message, data: d } });\n}\nreturn out;"
      },
      "name": "Verify signature + format",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        260,
        0
      ]
    },
    {
      "parameters": {},
      "name": "Send it (replace with WhatsApp / Notion / Gmail / Sheets / Slack)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        540,
        0
      ],
      "notes": "Use {{ $json.message }} as the text. Split by {{ $json.event }} with a Switch node if you want per-alert handling. Raw fields are in {{ $json.data }}.",
      "notesInFlow": true
    }
  ],
  "connections": {
    "FYP Now webhook": {
      "main": [
        [
          {
            "node": "Verify signature + format",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify signature + format": {
      "main": [
        [
          {
            "node": "Send it (replace with WhatsApp / Notion / Gmail / Sheets / Slack)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}
