Receive a webhook POST from an external service. Public endpoint — no authentication required. Security is provided by: - Unique, unguessable URL token - Optional HMAC signature verification - Rate limiting (5 requests/minute per token) - Payload size limit (64KB)
(token: str, request: Request)
| 44 | |
| 45 | @router.post("/t/{token}") |
| 46 | async def receive_webhook(token: str, request: Request): |
| 47 | """Receive a webhook POST from an external service. |
| 48 | |
| 49 | Public endpoint — no authentication required. |
| 50 | Security is provided by: |
| 51 | - Unique, unguessable URL token |
| 52 | - Optional HMAC signature verification |
| 53 | - Rate limiting (5 requests/minute per token) |
| 54 | - Payload size limit (64KB) |
| 55 | """ |
| 56 | # Rate limiting — use per-agent limit if available |
| 57 | hit_count = await _record_and_count_hits(token) |
| 58 | |
| 59 | # We'll check per-agent rate limit after finding the trigger below. |
| 60 | # For now, apply a generous global ceiling to prevent memory abuse. |
| 61 | if hit_count >= 60: # hard ceiling: 60/min regardless of config |
| 62 | logger.warning(f"Webhook hard rate limit exceeded for token {token[:8]}...") |
| 63 | return JSONResponse({"ok": True}, status_code=429) |
| 64 | |
| 65 | # Payload size check |
| 66 | body = await request.body() |
| 67 | if len(body) > MAX_PAYLOAD_SIZE: |
| 68 | logger.warning(f"Webhook payload too large for token {token[:8]}...: {len(body)} bytes") |
| 69 | return JSONResponse({"ok": True}, status_code=413) |
| 70 | |
| 71 | # Look up trigger |
| 72 | async with async_session() as db: |
| 73 | result = await db.execute( |
| 74 | select(AgentTrigger).where( |
| 75 | AgentTrigger.type == "webhook", |
| 76 | AgentTrigger.is_enabled, |
| 77 | ) |
| 78 | ) |
| 79 | triggers = result.scalars().all() |
| 80 | |
| 81 | # Find the trigger matching this token |
| 82 | target = None |
| 83 | for trigger in triggers: |
| 84 | cfg = trigger.config or {} |
| 85 | if cfg.get("token") == token: |
| 86 | target = trigger |
| 87 | break |
| 88 | |
| 89 | if not target: |
| 90 | # Return 200 OK to avoid leaking whether the token exists |
| 91 | return JSONResponse({"ok": True}) |
| 92 | |
| 93 | # Per-agent rate limit check |
| 94 | agent_result = await db.execute(select(Agent).where(Agent.id == target.agent_id)) |
| 95 | agent_obj = agent_result.scalar_one_or_none() |
| 96 | agent_rate_limit = (agent_obj.webhook_rate_limit if agent_obj else None) or RATE_LIMIT |
| 97 | |
| 98 | # Retrieve all needed scalar fields and expunge from db session to prevent MissingGreenlet errors. |
| 99 | target_name = target.name |
| 100 | target_agent_id = target.agent_id |
| 101 | target_config = target.config or {} |
| 102 | db.expunge(target) |
| 103 | if agent_obj: |
nothing calls this directly
no test coverage detected