{
  "nodes": [
    {
      "id": "a66a93f6-eed6-4565-815a-cf61df76f168",
      "name": "README",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3760,
        5632
      ],
      "parameters": {
        "width": 800,
        "height": 2256,
        "content": "# Webhook Rate Limiter (Guard)\n\nProtect public webhooks from burst traffic, abuse, and overload.\nUses **Ainoflow Guard** for edge-style rate decisions BEFORE expensive workflow logic executes.\n\n## Quick Start\n\n## 1. Setup Ainoflow\n### 1.1 Create API Key\n - https://www.ainoflow.io/signup (free plan available)\n### 1.2 Setup HTTP Bearer Credentials:\n - Ainoflow\n\n## 2. Configure Rate Limits\n### Edit **Config** node:\n - `rate_limit` \u2014 Max requests per window (default: 30)\n - `window_sec` \u2014 Window size in seconds (default: 60)\n - `identity_mode` \u2014 \"ip\" or \"apiKey\" (default: ip)\n - `route_name` \u2014 Logical endpoint name (default: webhook)\n\n## 3. Add Your Business Logic\n### Replace **BusinessLogic** node:\n - Access request body: `$('Webhook').first().json.body`\n - Access headers: `$('Webhook').first().json.headers`\n - Return your response data as JSON\n\n## 4. Test\n### Burst test (terminal):\n```\nfor i in {1..50}; do\n  curl -s -o /dev/null -w \"%{http_code}\\n\" \\\n    -X POST https://your-n8n.com/webhook/rate-limited-endpoint \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"test\": true}'\ndone\n```\n\n### Expected:\n - First 30 requests \u2192 200 OK\n - Remaining \u2192 429 Too Many Requests\n\n## 5. How It Works\n\n1. Webhook receives POST request\n2. Identity extracted (IP or API key)\n3. Guard checks rate limit (allow/deny)\n4. Allowed \u2192 Business Logic \u2192 200 OK\n5. Denied \u2192 429 + Retry-After header\n\n## 6. Architecture\n\n- **Fail-open**: Guard API uses `failOpen=true`\n  (Guard down \u2192 requests allowed through)\n- **Stateless**: No queues or databases needed in n8n\n- **Edge-style**: Decision before business logic\n- **Proxy-aware**: X-Forwarded-For support\n- Guard policy auto-creates on first request\n\n## 7. Identity Modes\n\n### IP mode (default)\n - Extracts client IP from X-Forwarded-For or x-real-ip\n - Works behind Cloudflare, nginx, load balancers\n - Identity key: `webhook:185.22.xx.xx`\n\n### API Key mode\n - Uses x-api-key header\n - Falls back to IP if header missing\n - Identity key: `webhook:client_abc123`\n\n## 8. Guard API Details\n\n - Endpoint: POST /api/v1/guard/{key}/counter\n - Policy auto-created with rateMax + rateWindow\n - returnSuccess=true \u2192 always 200 OK response\n - allowPolicyOverwrite=true \u2192 easy testing (set false in production)\n - Response: { allowed, remaining, resetsIn, rateLimit }\n\n## 9. Combine with Shield\nDuplicate webhooks? Add Ainoflow Shield for\none-trigger-one-execution guarantee.\nGuard + Shield = rate limiting + dedup.\n\n## 10. Need help?\nAinova Systems: https://ainovasystems.com/"
      },
      "typeVersion": 1
    },
    {
      "id": "4ca89072-57e6-493d-850e-726755c271d2",
      "name": "SectionRateLimitCheck",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4608,
        5632
      ],
      "parameters": {
        "color": 5,
        "width": 1360,
        "height": 652,
        "content": "## 1. Rate Limit Decision\nWebhook \u2192 Config \u2192 Build Identity \u2192 Guard Check \u2192 Allow or Deny"
      },
      "typeVersion": 1
    },
    {
      "id": "f238178d-2eed-4bfe-8650-6b39d0d23389",
      "name": "SectionAllowed",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6000,
        5632
      ],
      "parameters": {
        "color": 4,
        "width": 640,
        "height": 320,
        "content": "## 2. Allowed \u2192 Business Logic\nReplace **BusinessLogic** node with your workflow"
      },
      "typeVersion": 1
    },
    {
      "id": "ca85a539-3419-4dc7-9f8c-5da7f6003579",
      "name": "SectionDenied",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6000,
        5968
      ],
      "parameters": {
        "color": 3,
        "width": 640,
        "height": 320,
        "content": "## 3. Denied \u2192 429 Rate Limited\nImmediate rejection with Retry-After header"
      },
      "typeVersion": 1
    },
    {
      "id": "88e3710e-2bb8-4d9d-9fc9-1a269f4ba01e",
      "name": "StickyWebhook",
      "type": "n8n-nodes-base.stickyNote",
      "disabled": true,
      "position": [
        4640,
        5760
      ],
      "parameters": {
        "color": 7,
        "width": 224,
        "height": 416,
        "content": "## Webhook Entry\nPOST requests only.\nUses \"Respond to Webhook\" mode\nso workflow controls response timing."
      },
      "typeVersion": 1
    },
    {
      "id": "06c7ebe5-6636-4a36-ae7c-287e2d05239e",
      "name": "StickyConfig",
      "type": "n8n-nodes-base.stickyNote",
      "disabled": true,
      "position": [
        4896,
        5760
      ],
      "parameters": {
        "color": 2,
        "height": 416,
        "content": "## Configuration\nEdit values here to change\nrate limits and identity mode.\nNo code changes needed."
      },
      "typeVersion": 1
    },
    {
      "id": "66f62add-06b6-450e-a690-a40ca09789b8",
      "name": "StickyIdentity",
      "type": "n8n-nodes-base.stickyNote",
      "disabled": true,
      "position": [
        5168,
        5760
      ],
      "parameters": {
        "color": 2,
        "height": 416,
        "content": "## Identity Builder\nExtracts client identity from\nrequest headers.\nFormat: route:identity"
      },
      "typeVersion": 1
    },
    {
      "id": "63b60baa-6ef5-487c-b575-5d68daa91bb3",
      "name": "StickyGuard",
      "type": "n8n-nodes-base.stickyNote",
      "disabled": true,
      "position": [
        5440,
        5760
      ],
      "parameters": {
        "color": 2,
        "height": 416,
        "content": "## Guard Decision\nPOST to Guard API.\nPolicy auto-creates on first call.\nreturnSuccess=true \u2192 always 200.\n\n\u26a0\ufe0f allowPolicyOverwrite=true\nis set for easy testing.\nFor production: set to false\nto avoid hidden config drift."
      },
      "typeVersion": 1
    },
    {
      "id": "cd78a863-63ae-41ee-98b1-c3122dfa62bc",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        4704,
        6016
      ],
      "parameters": {
        "path": "rate-limited-endpoint",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "98d24922-4c1b-4b0d-9203-25cdde4a2edb",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "position": [
        4960,
        6016
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-rate-limit",
              "name": "rate_limit",
              "type": "number",
              "value": 30
            },
            {
              "id": "cfg-window-sec",
              "name": "window_sec",
              "type": "number",
              "value": 60
            },
            {
              "id": "cfg-identity-mode",
              "name": "identity_mode",
              "type": "string",
              "value": "ip"
            },
            {
              "id": "cfg-route-name",
              "name": "route_name",
              "type": "string",
              "value": "webhook"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "9ff885dd-a783-4813-894b-22a90aa2cf65",
      "name": "BuildIdentity",
      "type": "n8n-nodes-base.code",
      "position": [
        5232,
        6016
      ],
      "parameters": {
        "jsCode": "// Build identity key for Guard rate limiting\n// Priority: x-api-key \u2192 X-Forwarded-For[0] \u2192 x-real-ip \u2192 'unknown'\n\nconst headers = $('Webhook').first().json.headers;\nconst config = $input.first().json;\n\nlet identity;\n\nif (config.identity_mode === 'apiKey' && headers['x-api-key']) {\n  identity = headers['x-api-key'];\n} else {\n  const xff = headers['x-forwarded-for'];\n  if (xff) {\n    // Always use FIRST value (real client IP)\n    identity = xff.split(',')[0].trim();\n  } else {\n    identity = headers['x-real-ip'] || 'unknown';\n  }\n}\n\n// Format: route:identity (prevents cross-endpoint pollution)\nconst identityKey = `${config.route_name}:${identity}`;\n\nreturn [{\n  json: {\n    identity_key: identityKey,\n    identity: identity,\n    rate_limit: config.rate_limit,\n    window_sec: config.window_sec\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "c3bc52e5-b7b0-48f5-b353-6952c878cf64",
      "name": "GuardCheck",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        5504,
        6016
      ],
      "parameters": {
        "url": "=https://api.ainoflow.io/api/v1/guard/{{ encodeURIComponent($json.identity_key) }}/counter",
        "method": "POST",
        "options": {
          "timeout": 5000
        },
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "rateMax",
              "value": "={{ $json.rate_limit }}"
            },
            {
              "name": "rateWindow",
              "value": "={{ $json.window_sec }}"
            },
            {
              "name": "returnSuccess",
              "value": "true"
            },
            {
              "name": "failOpen",
              "value": "true"
            },
            {
              "name": "allowPolicyOverwrite",
              "value": "true"
            }
          ]
        }
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2,
      "alwaysOutputData": true
    },
    {
      "id": "b0f570b9-074b-4f6c-a13b-ec22b97349d6",
      "name": "IfAllowed",
      "type": "n8n-nodes-base.if",
      "position": [
        5776,
        6016
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "guard-allowed-check",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.allowed }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "b0fbb34c-5329-4d36-b1a8-2c6291989bd1",
      "name": "BusinessLogic",
      "type": "n8n-nodes-base.code",
      "position": [
        6080,
        5760
      ],
      "parameters": {
        "jsCode": "// ===== YOUR BUSINESS LOGIC HERE =====\n// Replace this node with your actual workflow logic.\n//\n// Access original webhook data:\n//   const body = $('Webhook').first().json.body;\n//   const headers = $('Webhook').first().json.headers;\n//   const query = $('Webhook').first().json.query;\n//\n// Guard decision details (from GuardCheck):\n//   const remaining = $('GuardCheck').first().json.remaining;\n//   const resetsIn = $('GuardCheck').first().json.resetsIn;\n\nconst body = $('Webhook').first().json.body;\n\nreturn [{\n  json: {\n    ok: true,\n    data: body || { message: \"Request processed successfully\" }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "09b05b88-3695-499d-8401-432390111f48",
      "name": "RespondOk",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        6288,
        5760
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "abbf7d98-4ba5-436d-9ffc-d27f26a7bc38",
      "name": "BuildDeniedResponse",
      "type": "n8n-nodes-base.set",
      "position": [
        6096,
        6096
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "denied-ok",
              "name": "ok",
              "type": "boolean",
              "value": false
            },
            {
              "id": "denied-error",
              "name": "error",
              "type": "string",
              "value": "rate_limited"
            },
            {
              "id": "denied-retry",
              "name": "retryAfter",
              "type": "number",
              "value": "={{ $json.resetsIn }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "40764d73-42fd-4730-ad69-a83a2bc7f65b",
      "name": "RespondRateLimited",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        6288,
        6096
      ],
      "parameters": {
        "options": {
          "responseCode": 429,
          "responseHeaders": {
            "entries": [
              {
                "name": "Retry-After",
                "value": "={{ $json.retryAfter }}"
              }
            ]
          }
        }
      },
      "typeVersion": 1.1
    }
  ],
  "connections": {
    "Config": {
      "main": [
        [
          {
            "node": "BuildIdentity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IfAllowed": {
      "main": [
        [
          {
            "node": "BusinessLogic",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "BuildDeniedResponse",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GuardCheck": {
      "main": [
        [
          {
            "node": "IfAllowed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "BuildIdentity": {
      "main": [
        [
          {
            "node": "GuardCheck",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "BusinessLogic": {
      "main": [
        [
          {
            "node": "RespondOk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "BuildDeniedResponse": {
      "main": [
        [
          {
            "node": "RespondRateLimited",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}